diff options
Diffstat (limited to '')
-rw-r--r-- | src/lib-imap/imap-base-subject.c | 248 |
1 files changed, 248 insertions, 0 deletions
diff --git a/src/lib-imap/imap-base-subject.c b/src/lib-imap/imap-base-subject.c new file mode 100644 index 0000000..02469d3 --- /dev/null +++ b/src/lib-imap/imap-base-subject.c @@ -0,0 +1,248 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +/* Implemented against draft-ietf-imapext-sort-10 and + draft-ietf-imapext-thread-12 */ + +#include "lib.h" +#include "buffer.h" +#include "charset-utf8.h" +#include "message-header-decode.h" +#include "imap-base-subject.h" + +static void pack_whitespace(buffer_t *buf) +{ + char *data, *dest; + bool last_lwsp; + + data = buffer_get_modifiable_data(buf, NULL); + + /* check if we need to do anything */ + while (*data != '\0') { + if (*data == '\t' || *data == '\n' || *data == '\r' || + (*data == ' ' && (data[1] == ' ' || data[1] == '\t'))) + break; + data++; + } + + if (*data == '\0') + return; + + /* @UNSAFE: convert/pack the whitespace */ + dest = data; last_lwsp = FALSE; + while (*data != '\0') { + if (*data == '\t' || *data == ' ' || + *data == '\r' || *data == '\n') { + if (!last_lwsp) { + *dest++ = ' '; + last_lwsp = TRUE; + } + } else { + *dest++ = *data; + last_lwsp = FALSE; + } + data++; + } + *dest = '\0'; + + data = buffer_get_modifiable_data(buf, NULL); + buffer_set_used_size(buf, (size_t) (dest - data)+1); +} + +static void remove_subj_trailers(buffer_t *buf, size_t start_pos, + bool *is_reply_or_forward_r) +{ + const char *data; + size_t orig_size, size; + + /* subj-trailer = "(fwd)" / WSP */ + data = buffer_get_data(buf, &orig_size); + + if (orig_size < 1) /* size includes trailing \0 */ + return; + + for (size = orig_size-1; size > start_pos; ) { + if (data[size-1] == ' ') + size--; + else if (size >= 5 && + memcmp(data + size - 5, "(FWD)", 5) == 0) { + *is_reply_or_forward_r = TRUE; + size -= 5; + } else { + break; + } + } + + if (size != orig_size-1) { + buffer_set_used_size(buf, size); + buffer_append_c(buf, '\0'); + } +} + +static bool remove_blob(const char **datap) +{ + const char *data = *datap; + + if (*data != '[') + return FALSE; + + data++; + while (*data != '\0' && *data != '[' && *data != ']') + data++; + + if (*data != ']') + return FALSE; + + data++; + if (*data == ' ') + data++; + + *datap = data; + return TRUE; +} + +static bool remove_subj_leader(buffer_t *buf, size_t *start_pos, + bool *is_reply_or_forward_r) +{ + const char *data, *orig_data; + bool ret = FALSE; + + /* subj-leader = (*subj-blob subj-refwd) / WSP + + subj-blob = "[" *BLOBCHAR "]" *WSP + subj-refwd = ("re" / ("fw" ["d"])) *WSP [subj-blob] ":" + + BLOBCHAR = %x01-5a / %x5c / %x5e-7f + ; any CHAR except '[' and ']' */ + orig_data = buf->data; + orig_data += *start_pos; + data = orig_data; + + if (*data == ' ') { + /* independent from checks below - always removed */ + data++; orig_data++; + *start_pos += 1; + ret = TRUE; + } + + while (*data == '[') { + if (!remove_blob(&data)) + return ret; + } + + if (str_begins(data, "RE")) + data += 2; + else if (str_begins(data, "FWD")) + data += 3; + else if (str_begins(data, "FW")) + data += 2; + else + return ret; + + if (*data == ' ') + data++; + + if (*data == '[' && !remove_blob(&data)) + return ret; + + if (*data != ':') + return ret; + + data++; + *start_pos += (size_t)(data - orig_data); + *is_reply_or_forward_r = TRUE; + return TRUE; +} + +static bool remove_blob_when_nonempty(buffer_t *buf, size_t *start_pos) +{ + const char *data, *orig_data; + + orig_data = buf->data; + orig_data += *start_pos; + data = orig_data; + if (*data == '[' && remove_blob(&data) && *data != '\0') { + *start_pos += (size_t)(data - orig_data); + return TRUE; + } + + return FALSE; +} + +static bool remove_subj_fwd_hdr(buffer_t *buf, size_t *start_pos, + bool *is_reply_or_forward_r) +{ + const char *data = buf->data; + size_t size = buf->used; + + /* subj-fwd = subj-fwd-hdr subject subj-fwd-trl + subj-fwd-hdr = "[fwd:" + subj-fwd-trl = "]" */ + + if (!str_begins(data + *start_pos, "[FWD:")) + return FALSE; + + if (data[size-2] != ']') + return FALSE; + + *is_reply_or_forward_r = TRUE; + + buffer_set_used_size(buf, size-2); + buffer_append_c(buf, '\0'); + + *start_pos += 5; + return TRUE; +} + +const char *imap_get_base_subject_cased(pool_t pool, const char *subject, + bool *is_reply_or_forward_r) +{ + buffer_t *buf; + size_t start_pos, subject_len; + bool found; + + *is_reply_or_forward_r = FALSE; + + subject_len = strlen(subject); + buf = buffer_create_dynamic(pool, subject_len); + + /* (1) Convert any RFC 2047 encoded-words in the subject to + UTF-8. Convert all tabs and continuations to space. + Convert all multiple spaces to a single space. */ + message_header_decode_utf8((const unsigned char *)subject, subject_len, + buf, uni_utf8_to_decomposed_titlecase); + buffer_append_c(buf, '\0'); + + pack_whitespace(buf); + + start_pos = 0; + do { + /* (2) Remove all trailing text of the subject that matches + the subj-trailer ABNF, repeat until no more matches are + possible. */ + remove_subj_trailers(buf, start_pos, is_reply_or_forward_r); + + do { + /* (3) Remove all prefix text of the subject that + matches the subj-leader ABNF. */ + found = remove_subj_leader(buf, &start_pos, + is_reply_or_forward_r); + + /* (4) If there is prefix text of the subject that + matches the subj-blob ABNF, and removing that prefix + leaves a non-empty subj-base, then remove the prefix + text. */ + found = remove_blob_when_nonempty(buf, &start_pos) || + found; + + /* (5) Repeat (3) and (4) until no matches remain. */ + } while (found); + + /* (6) If the resulting text begins with the subj-fwd-hdr ABNF + and ends with the subj-fwd-trl ABNF, remove the + subj-fwd-hdr and subj-fwd-trl and repeat from step (2). */ + } while (remove_subj_fwd_hdr(buf, &start_pos, is_reply_or_forward_r)); + + /* (7) The resulting text is the "base subject" used in the + SORT. */ + return (const char *)buf->data + start_pos; +} |