summaryrefslogtreecommitdiffstats
path: root/src/pop3/pop3-commands.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/pop3/pop3-commands.c977
1 files changed, 977 insertions, 0 deletions
diff --git a/src/pop3/pop3-commands.c b/src/pop3/pop3-commands.c
new file mode 100644
index 0000000..2619dab
--- /dev/null
+++ b/src/pop3/pop3-commands.c
@@ -0,0 +1,977 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "pop3-common.h"
+#include "array.h"
+#include "istream.h"
+#include "ostream.h"
+#include "hash.h"
+#include "str.h"
+#include "var-expand.h"
+#include "message-size.h"
+#include "mail-storage.h"
+#include "mail-storage-settings.h"
+#include "mail-search-build.h"
+#include "pop3-capability.h"
+#include "pop3-commands.h"
+
+static enum mail_sort_type pop3_sort_program[] = {
+ MAIL_SORT_POP3_ORDER,
+ MAIL_SORT_END
+};
+
+static uint32_t msgnum_to_seq(struct client *client, uint32_t msgnum)
+{
+ return msgnum < client->msgnum_to_seq_map_count ?
+ client->msgnum_to_seq_map[msgnum] : msgnum+1;
+}
+
+static const char *get_msgnum(struct client *client, const char *args,
+ unsigned int *msgnum, bool thenspace)
+{
+ unsigned int num;
+
+ if (*args < '0' || *args > '9') {
+ client_send_line(client,
+ "-ERR Invalid message number: %s", args);
+ return NULL;
+ }
+ if (str_parse_uint(args, &num, &args) < 0) {
+ client_send_line(client,
+ "-ERR Message number too large: %s", args);
+ return NULL;
+ }
+ if (*args != (thenspace ? ' ' : '\0')) {
+ client_send_line(client,
+ "-ERR Noise after message number: %s", args);
+ return NULL;
+ }
+ if (num == 0 || num > client->messages_count) {
+ client_send_line(client,
+ "-ERR There's no message %u.", num);
+ return NULL;
+ }
+ num--;
+
+ if (client->deleted) {
+ if ((client->deleted_bitmask[num / CHAR_BIT] &
+ (1 << (num % CHAR_BIT))) != 0) {
+ client_send_line(client, "-ERR Message is deleted.");
+ return NULL;
+ }
+ }
+
+ while (*args == ' ') args++;
+
+ *msgnum = num;
+ return args;
+}
+
+static const char *get_size(struct client *client, const char *args,
+ uoff_t *size, bool thenspace)
+{
+ uoff_t num;
+
+ if (*args < '0' || *args > '9') {
+ client_send_line(client, "-ERR Invalid size: %s",
+ args);
+ return NULL;
+ }
+ if (str_parse_uoff(args, &num, &args) < 0) {
+ client_send_line(client, "-ERR Size too large: %s",
+ args);
+ return NULL;
+ }
+ if (*args != (thenspace ? ' ' : '\0')) {
+ client_send_line(client, "-ERR Noise after size: %s", args);
+ return NULL;
+ }
+
+ while (*args == ' ') args++;
+
+ *size = num;
+ return args;
+}
+
+static int cmd_capa(struct client *client, const char *args ATTR_UNUSED)
+{
+ client_send_line(client, "+OK\r\n"POP3_CAPABILITY_REPLY".");
+ return 1;
+}
+
+static int cmd_dele(struct client *client, const char *args)
+{
+ unsigned int msgnum;
+
+ if (get_msgnum(client, args, &msgnum, FALSE) == NULL)
+ return -1;
+
+ if (!client->deleted) {
+ client->deleted_bitmask = i_malloc(MSGS_BITMASK_SIZE(client));
+ client->deleted = TRUE;
+ }
+
+ client->deleted_bitmask[msgnum / CHAR_BIT] |= 1 << (msgnum % CHAR_BIT);
+ client->deleted_count++;
+ client->deleted_size += client->message_sizes[msgnum];
+ client_send_line(client, "+OK Marked to be deleted.");
+ return 1;
+}
+
+struct cmd_list_context {
+ unsigned int msgnum;
+};
+
+static void cmd_list_callback(struct client *client)
+{
+ struct cmd_list_context *ctx = client->cmd_context;
+
+ for (; ctx->msgnum != client->messages_count; ctx->msgnum++) {
+ if (client->output->closed)
+ break;
+ if (POP3_CLIENT_OUTPUT_FULL(client)) {
+ /* buffer full */
+ return;
+ }
+
+ if (client->deleted) {
+ if ((client->deleted_bitmask[ctx->msgnum / CHAR_BIT] &
+ (1 << (ctx->msgnum % CHAR_BIT))) != 0)
+ continue;
+ }
+
+ client_send_line(client, "%u %"PRIuUOFF_T, ctx->msgnum+1,
+ client->message_sizes[ctx->msgnum]);
+ }
+
+ client_send_line(client, ".");
+
+ i_free(ctx);
+ client->cmd = NULL;
+}
+
+static int cmd_list(struct client *client, const char *args)
+{
+ struct cmd_list_context *ctx;
+
+ if (*args == '\0') {
+ ctx = i_new(struct cmd_list_context, 1);
+ client_send_line(client, "+OK %u messages:",
+ client->messages_count - client->deleted_count);
+
+ client->cmd = cmd_list_callback;
+ client->cmd_context = ctx;
+ cmd_list_callback(client);
+ } else {
+ unsigned int msgnum;
+
+ if (get_msgnum(client, args, &msgnum, FALSE) == NULL)
+ return -1;
+
+ client_send_line(client, "+OK %u %"PRIuUOFF_T, msgnum+1,
+ client->message_sizes[msgnum]);
+ }
+
+ return 1;
+}
+
+static int cmd_last(struct client *client, const char *args ATTR_UNUSED)
+{
+ client_send_line(client, "+OK %u", client->last_seen_pop3_msn);
+ return 1;
+}
+
+static int cmd_noop(struct client *client, const char *args ATTR_UNUSED)
+{
+ client_send_line(client, "+OK");
+ return 1;
+}
+
+static struct mail_search_args *
+pop3_search_build_seqset(ARRAY_TYPE(seq_range) *seqset)
+{
+ struct mail_search_args *search_args;
+ struct mail_search_arg *sarg;
+
+ search_args = mail_search_build_init();
+ sarg = mail_search_build_add(search_args, SEARCH_SEQSET);
+ sarg->value.seqset = *seqset;
+ return search_args;
+}
+
+static struct mail_search_args *
+pop3_search_build(struct client *client, uint32_t seq)
+{
+ struct mail_search_args *search_args;
+
+ if (seq == 0)
+ return pop3_search_build_seqset(&client->all_seqs);
+
+ search_args = mail_search_build_init();
+ mail_search_build_add_seqset(search_args, seq, seq);
+ return search_args;
+}
+
+static int client_verify_ordering(struct client *client,
+ struct mail *mail, uint32_t msgnum)
+{
+ uint32_t seq;
+
+ seq = msgnum_to_seq(client, msgnum);
+ if (seq != mail->seq) {
+ i_error("Message ordering changed unexpectedly "
+ "(msg #%u: storage seq %u -> %u)",
+ msgnum+1, seq, mail->seq);
+ return -1;
+ }
+ return 0;
+}
+
+static void client_expunge(struct client *client, struct mail *mail)
+{
+ switch (client->set->parsed_delete_type) {
+ case POP3_DELETE_TYPE_EXPUNGE:
+ mail_expunge(mail);
+ break;
+ case POP3_DELETE_TYPE_FLAG:
+ i_assert(client->deleted_kw != NULL);
+ mail_update_keywords(mail, MODIFY_ADD, client->deleted_kw);
+ break;
+ }
+}
+
+bool client_update_mails(struct client *client)
+{
+ struct mail_search_args *search_args;
+ struct mail_search_context *ctx;
+ struct mail *mail;
+ ARRAY_TYPE(seq_range) deleted_msgs, seen_msgs;
+ uint32_t msgnum, bit;
+ bool ret = TRUE;
+
+ if (mailbox_is_readonly(client->mailbox)) {
+ /* silently ignore */
+ return TRUE;
+ }
+
+ /* translate msgnums to sequences (in case POP3 ordering is
+ different) */
+ t_array_init(&deleted_msgs, 8);
+ if (client->deleted_bitmask != NULL && client->quit_seen) {
+ for (msgnum = 0; msgnum < client->messages_count; msgnum++) {
+ bit = 1 << (msgnum % CHAR_BIT);
+ if ((client->deleted_bitmask[msgnum / CHAR_BIT] & bit) != 0)
+ seq_range_array_add(&deleted_msgs, msgnum_to_seq(client, msgnum));
+ }
+ }
+ t_array_init(&seen_msgs, 8);
+ if (client->seen_bitmask != NULL) {
+ for (msgnum = 0; msgnum < client->messages_count; msgnum++) {
+ bit = 1 << (msgnum % CHAR_BIT);
+ if ((client->seen_bitmask[msgnum / CHAR_BIT] & bit) != 0)
+ seq_range_array_add(&seen_msgs, msgnum_to_seq(client, msgnum));
+ }
+ }
+
+ if (array_count(&deleted_msgs) > 0) {
+ /* expunge DELEted mails */
+ search_args = pop3_search_build_seqset(&deleted_msgs);
+ ctx = mailbox_search_init(client->trans, search_args, NULL, 0, NULL);
+ mail_search_args_unref(&search_args);
+
+ while (mailbox_search_next(ctx, &mail))
+ client_expunge(client, mail);
+ if (mailbox_search_deinit(&ctx) < 0)
+ ret = FALSE;
+ /* don't bother setting \Seen flags for deleted messages */
+ seq_range_array_invert(&deleted_msgs, 1, client->highest_seq);
+ seq_range_array_intersect(&seen_msgs, &deleted_msgs);
+ }
+
+ if (array_count(&seen_msgs) > 0) {
+ /* add \Seen flags for RETRed mails */
+ search_args = pop3_search_build_seqset(&seen_msgs);
+ ctx = mailbox_search_init(client->trans, search_args, NULL, 0, NULL);
+ mail_search_args_unref(&search_args);
+
+ while (mailbox_search_next(ctx, &mail))
+ mail_update_flags(mail, MODIFY_ADD, MAIL_SEEN);
+ if (mailbox_search_deinit(&ctx) < 0)
+ ret = FALSE;
+ }
+
+ client->seen_change_count = 0;
+ return ret;
+}
+
+static int cmd_quit(struct client *client, const char *args ATTR_UNUSED)
+{
+ client->quit_seen = TRUE;
+ if (client->deleted || client->seen_bitmask != NULL) {
+ if (!client_update_mails(client)) {
+ client_send_storage_error(client);
+ client_disconnect(client,
+ "Storage error during logout.");
+ return 1;
+ }
+ }
+
+ if (mailbox_transaction_commit(&client->trans) < 0 ||
+ mailbox_sync(client->mailbox, MAILBOX_SYNC_FLAG_FULL_WRITE) < 0) {
+ client_send_storage_error(client);
+ client_disconnect(client, "Storage error during logout.");
+ return 1;
+ } else {
+ client->delete_success = TRUE;
+ }
+
+ if (!client->deleted)
+ client_send_line(client, "+OK Logging out.");
+ else
+ client_send_line(client, "+OK Logging out, messages deleted.");
+
+ client_disconnect(client, "Logged out");
+ return 1;
+}
+
+struct fetch_context {
+ struct mail *mail;
+ struct istream *stream;
+ uoff_t body_lines;
+
+ uoff_t *byte_counter;
+ uoff_t byte_counter_offset;
+
+ unsigned char last;
+ bool cr_skipped, in_body;
+};
+
+static void fetch_deinit(struct fetch_context *ctx)
+{
+ mail_free(&ctx->mail);
+ i_free(ctx);
+}
+
+static void fetch_callback(struct client *client)
+{
+ struct fetch_context *ctx = client->cmd_context;
+ const unsigned char *data;
+ unsigned char add;
+ size_t i, size;
+ int ret;
+
+ while ((ctx->body_lines > 0 || !ctx->in_body) &&
+ i_stream_read_more(ctx->stream, &data, &size) > 0) {
+ if (size > 4096)
+ size = 4096;
+
+ add = '\0';
+ for (i = 0; i < size; i++) {
+ if ((data[i] == '\r' || data[i] == '\n') &&
+ !ctx->in_body) {
+ if (i == 0 && (ctx->last == '\0' ||
+ ctx->last == '\n'))
+ ctx->in_body = TRUE;
+ else if (i > 0 && data[i-1] == '\n')
+ ctx->in_body = TRUE;
+ }
+
+ if (data[i] == '\n') {
+ if ((i == 0 && ctx->last != '\r') ||
+ (i > 0 && data[i-1] != '\r')) {
+ /* missing CR */
+ add = '\r';
+ break;
+ }
+
+ if (ctx->in_body) {
+ if (--ctx->body_lines == 0) {
+ i++;
+ break;
+ }
+ }
+ } else if (data[i] == '.' &&
+ ((i == 0 && ctx->last == '\n') ||
+ (i > 0 && data[i-1] == '\n'))) {
+ /* escape the dot */
+ add = '.';
+ break;
+ } else if (data[i] == '\0' &&
+ (client->set->parsed_workarounds &
+ WORKAROUND_OUTLOOK_NO_NULS) != 0) {
+ add = 0x80;
+ break;
+ }
+ }
+
+ if (i > 0) {
+ if (o_stream_send(client->output, data, i) < 0)
+ break;
+ ctx->last = data[i-1];
+ i_stream_skip(ctx->stream, i);
+ }
+
+ if (o_stream_get_buffer_used_size(client->output) >= 4096) {
+ if ((ret = o_stream_flush(client->output)) < 0)
+ break;
+ if (ret == 0) {
+ /* continue later */
+ return;
+ }
+ }
+
+ if (add != '\0') {
+ if (o_stream_send(client->output, &add, 1) < 0)
+ break;
+
+ ctx->last = add;
+ if (add == 0x80)
+ i_stream_skip(ctx->stream, 1);
+ }
+ }
+
+ if (ctx->last != '\n') {
+ /* didn't end with CRLF */
+ o_stream_nsend(client->output, "\r\n", 2);
+ }
+
+ if (!ctx->in_body &&
+ (client->set->parsed_workarounds & WORKAROUND_OE_NS_EOH) != 0) {
+ /* Add the missing end of headers line. */
+ o_stream_nsend(client->output, "\r\n", 2);
+ }
+
+ *ctx->byte_counter +=
+ client->output->offset - ctx->byte_counter_offset;
+
+ client_send_line(client, ".");
+ fetch_deinit(ctx);
+ client->cmd = NULL;
+}
+
+static int client_reply_msg_expunged(struct client *client, unsigned int msgnum)
+{
+ client_send_line(client, "-ERR Message %u expunged.", msgnum + 1);
+ if (msgnum <= client->highest_expunged_fetch_msgnum) {
+ /* client tried to fetch an expunged message again.
+ treat this as error so we'll eventually disconnect the
+ client instead of letting it loop forever. */
+ return -1;
+ }
+ client->highest_expunged_fetch_msgnum = msgnum;
+ return 1;
+}
+
+static int fetch(struct client *client, unsigned int msgnum, uoff_t body_lines,
+ const char *reason, uoff_t *byte_counter)
+{
+ struct fetch_context *ctx;
+ int ret;
+
+ ctx = i_new(struct fetch_context, 1);
+ ctx->byte_counter = byte_counter;
+ ctx->byte_counter_offset = client->output->offset;
+ ctx->mail = mail_alloc(client->trans,
+ MAIL_FETCH_STREAM_HEADER |
+ MAIL_FETCH_STREAM_BODY, NULL);
+ mail_set_seq(ctx->mail, msgnum_to_seq(client, msgnum));
+
+ if (mail_get_stream_because(ctx->mail, NULL, NULL, reason, &ctx->stream) < 0) {
+ ret = client_reply_msg_expunged(client, msgnum);
+ fetch_deinit(ctx);
+ return ret;
+ }
+
+ if (body_lines == UOFF_T_MAX && client->seen_bitmask != NULL) {
+ if ((mail_get_flags(ctx->mail) & MAIL_SEEN) == 0) {
+ /* mark the message seen with RETR command */
+ client->seen_bitmask[msgnum / CHAR_BIT] |=
+ 1 << (msgnum % CHAR_BIT);
+ client->seen_change_count++;
+ }
+ }
+
+ ctx->body_lines = body_lines;
+ if (body_lines == UOFF_T_MAX) {
+ client_send_line(client, "+OK %"PRIuUOFF_T" octets",
+ client->message_sizes[msgnum]);
+ } else {
+ client_send_line(client, "+OK");
+ ctx->body_lines++; /* internally we count the empty line too */
+ }
+
+ client->cmd = fetch_callback;
+ client->cmd_context = ctx;
+ fetch_callback(client);
+ return 1;
+}
+
+static int cmd_retr(struct client *client, const char *args)
+{
+ unsigned int msgnum;
+
+ if (get_msgnum(client, args, &msgnum, FALSE) == NULL)
+ return -1;
+
+ if (client->lowest_retr_pop3_msn > msgnum+1 ||
+ client->lowest_retr_pop3_msn == 0)
+ client->lowest_retr_pop3_msn = msgnum+1;
+ if (client->last_seen_pop3_msn < msgnum+1)
+ client->last_seen_pop3_msn = msgnum+1;
+
+ client->retr_count++;
+ return fetch(client, msgnum, UOFF_T_MAX, "RETR", &client->retr_bytes);
+}
+
+static int cmd_rset(struct client *client, const char *args ATTR_UNUSED)
+{
+ struct mail_search_context *search_ctx;
+ struct mail *mail;
+ struct mail_search_args *search_args;
+
+ client->last_seen_pop3_msn = 0;
+
+ if (client->deleted) {
+ client->deleted = FALSE;
+ memset(client->deleted_bitmask, 0, MSGS_BITMASK_SIZE(client));
+ client->deleted_count = 0;
+ client->deleted_size = 0;
+ }
+ if (client->seen_change_count > 0) {
+ memset(client->seen_bitmask, 0, MSGS_BITMASK_SIZE(client));
+ client->seen_change_count = 0;
+ }
+
+ if (client->set->pop3_enable_last) {
+ /* remove all \Seen flags (as specified by RFC 1460) */
+ search_args = pop3_search_build(client, 0);
+ search_ctx = mailbox_search_init(client->trans,
+ search_args, NULL, 0, NULL);
+ mail_search_args_unref(&search_args);
+
+ while (mailbox_search_next(search_ctx, &mail))
+ mail_update_flags(mail, MODIFY_REMOVE, MAIL_SEEN);
+ (void)mailbox_search_deinit(&search_ctx);
+
+ (void)mailbox_transaction_commit(&client->trans);
+ client->trans = mailbox_transaction_begin(client->mailbox, 0,
+ __func__);
+ }
+
+ client_send_line(client, "+OK");
+ return 1;
+}
+
+static int cmd_stat(struct client *client, const char *args ATTR_UNUSED)
+{
+ client_send_line(client, "+OK %u %"PRIuUOFF_T,
+ client->messages_count - client->deleted_count,
+ client->total_size - client->deleted_size);
+ return 1;
+}
+
+static int cmd_top(struct client *client, const char *args)
+{
+ unsigned int msgnum;
+ uoff_t max_lines;
+
+ args = get_msgnum(client, args, &msgnum, TRUE);
+ if (args == NULL)
+ return -1;
+ if (get_size(client, args, &max_lines, FALSE) == NULL)
+ return -1;
+
+ client->top_count++;
+ return fetch(client, msgnum, max_lines, "TOP", &client->top_bytes);
+}
+
+struct cmd_uidl_context {
+ struct mail_search_context *search_ctx;
+ struct mail *mail;
+ uint32_t msgnum;
+ bool list_all;
+};
+
+static int
+pop3_get_uid(struct client *client, struct mail *mail, string_t *str,
+ bool *permanent_uidl_r)
+{
+ char uid_str[MAX_INT_STRLEN] = { 0 };
+ const char *uidl;
+ const char *hdr_md5 = NULL, *filename = NULL, *guid = NULL;
+
+ if (mail_get_special(mail, MAIL_FETCH_UIDL_BACKEND, &uidl) == 0 &&
+ *uidl != '\0') {
+ str_append(str, uidl);
+ /* UIDL is already permanent */
+ *permanent_uidl_r = TRUE;
+ return 0;
+ }
+
+ *permanent_uidl_r = FALSE;
+
+ if (client->set->pop3_reuse_xuidl &&
+ mail_get_first_header(mail, "X-UIDL", &uidl) > 0) {
+ str_append(str, uidl);
+ return 0;
+ }
+
+ if ((client->uidl_keymask & UIDL_UID) != 0) {
+ if (i_snprintf(uid_str, sizeof(uid_str), "%u", mail->uid) < 0)
+ i_unreached();
+ }
+ if ((client->uidl_keymask & UIDL_MD5) != 0) {
+ if (mail_get_special(mail, MAIL_FETCH_HEADER_MD5,
+ &hdr_md5) < 0) {
+ i_error("UIDL: Header MD5 lookup failed: %s",
+ mailbox_get_last_internal_error(mail->box, NULL));
+ return -1;
+ } else if (hdr_md5[0] == '\0') {
+ i_error("UIDL: Header MD5 not found "
+ "(pop3_uidl_format=%%m not supported by storage?)");
+ return -1;
+ }
+ }
+ if ((client->uidl_keymask & UIDL_FILE_NAME) != 0) {
+ if (mail_get_special(mail, MAIL_FETCH_STORAGE_ID,
+ &filename) < 0) {
+ i_error("UIDL: File name lookup failed: %s",
+ mailbox_get_last_internal_error(mail->box, NULL));
+ return -1;
+ } else if (filename[0] == '\0') {
+ i_error("UIDL: File name not found "
+ "(pop3_uidl_format=%%f not supported by storage?)");
+ return -1;
+ }
+ }
+ if ((client->uidl_keymask & UIDL_GUID) != 0) {
+ if (mail_get_special(mail, MAIL_FETCH_GUID,
+ &guid) < 0) {
+ i_error("UIDL: Message GUID lookup failed: %s",
+ mailbox_get_last_internal_error(mail->box, NULL));
+ return -1;
+ } else if (guid[0] == '\0') {
+ i_error("UIDL: Message GUID not found "
+ "(pop3_uidl_format=%%g not supported by storage?)");
+ return -1;
+ }
+ }
+
+ const struct var_expand_table tab[] = {
+ { 'v', dec2str(client->uid_validity), "uidvalidity" },
+ { 'u', uid_str, "uid" },
+ { 'm', hdr_md5, "md5" },
+ { 'f', filename, "filename" },
+ { 'g', guid, "guid" },
+ { '\0', NULL, NULL }
+ };
+ const char *error;
+
+ if (var_expand(str, client->mail_set->pop3_uidl_format,
+ tab, &error) <= 0) {
+ i_error("UIDL: Failed to expand pop3_uidl_format=%s: %s",
+ client->mail_set->pop3_uidl_format, error);
+ return -1;
+ }
+ return 0;
+}
+
+static bool
+list_uidls_saved_iter(struct client *client, struct cmd_uidl_context *ctx)
+{
+ bool found = FALSE;
+
+ while (ctx->msgnum < client->messages_count) {
+ uint32_t msgnum = ctx->msgnum++;
+
+ if (client->deleted) {
+ if ((client->deleted_bitmask[msgnum / CHAR_BIT] &
+ (1 << (msgnum % CHAR_BIT))) != 0)
+ continue;
+ }
+ found = TRUE;
+
+ client_send_line(client,
+ ctx->list_all ? "%u %s" : "+OK %u %s",
+ msgnum+1, client->message_uidls[msgnum]);
+ if (client->output->closed || !ctx->list_all)
+ break;
+ if (POP3_CLIENT_OUTPUT_FULL(client)) {
+ /* output is being buffered, continue when there's
+ more space */
+ return FALSE;
+ }
+ }
+ /* finished */
+ client->cmd = NULL;
+
+ if (ctx->list_all)
+ client_send_line(client, ".");
+ i_free(ctx);
+ return found;
+}
+
+static bool list_uids_iter(struct client *client, struct cmd_uidl_context *ctx)
+{
+ string_t *str;
+ bool permanent_uidl, found = FALSE;
+ bool failed = FALSE;
+
+ if (client->message_uidls != NULL)
+ return list_uidls_saved_iter(client, ctx);
+
+ str = t_str_new(128);
+ while (mailbox_search_next(ctx->search_ctx, &ctx->mail)) {
+ uint32_t msgnum = ctx->msgnum++;
+
+ if (client_verify_ordering(client, ctx->mail, msgnum) < 0) {
+ failed = TRUE;
+ break;
+ }
+ if (client->deleted) {
+ if ((client->deleted_bitmask[msgnum / CHAR_BIT] &
+ (1 << (msgnum % CHAR_BIT))) != 0)
+ continue;
+ }
+ found = TRUE;
+
+ str_truncate(str, 0);
+ if (pop3_get_uid(client, ctx->mail, str, &permanent_uidl) < 0) {
+ failed = TRUE;
+ break;
+ }
+ if (client->set->pop3_save_uidl && !permanent_uidl)
+ mail_update_pop3_uidl(ctx->mail, str_c(str));
+
+ client_send_line(client, ctx->list_all ? "%u %s" : "+OK %u %s",
+ msgnum+1, str_c(str));
+ if (client->output->closed)
+ break;
+ if (POP3_CLIENT_OUTPUT_FULL(client) && ctx->list_all) {
+ /* output is being buffered, continue when there's
+ more space */
+ return FALSE;
+ }
+ }
+
+ /* finished */
+ (void)mailbox_search_deinit(&ctx->search_ctx);
+
+ client->cmd = NULL;
+
+ if (ctx->list_all && !failed)
+ client_send_line(client, ".");
+ i_free(ctx);
+ if (failed)
+ client_disconnect(client, "POP3 UIDLs couldn't be listed");
+ return found || failed;
+}
+
+static void cmd_uidl_callback(struct client *client)
+{
+ struct cmd_uidl_context *ctx = client->cmd_context;
+
+ (void)list_uids_iter(client, ctx);
+}
+
+HASH_TABLE_DEFINE_TYPE(uidl_counter, char *, void *);
+
+static void
+uidl_rename_duplicate(string_t *uidl, HASH_TABLE_TYPE(uidl_counter) prev_uidls)
+{
+ char *key;
+ void *value;
+ unsigned int counter;
+
+ while (hash_table_lookup_full(prev_uidls, str_c(uidl), &key, &value)) {
+ /* duplicate. the value contains the number of duplicates. */
+ counter = POINTER_CAST_TO(value, unsigned int) + 1;
+ hash_table_update(prev_uidls, key, POINTER_CAST(counter));
+ str_printfa(uidl, "-%u", counter);
+ /* the second lookup really should return NULL, but just in
+ case of some weird UIDLs do this as many times as needed */
+ }
+}
+
+static void client_uidls_save(struct client *client)
+{
+ struct mail_search_context *search_ctx;
+ struct mail_search_args *search_args;
+ struct mail *mail;
+ HASH_TABLE_TYPE(uidl_counter) prev_uidls;
+ const char **seq_uidls;
+ string_t *str;
+ char *uidl;
+ enum mail_fetch_field wanted_fields;
+ uint32_t msgnum;
+ bool permanent_uidl, uidl_duplicates_rename, failed = FALSE;
+
+ i_assert(client->message_uidls == NULL);
+
+ search_args = pop3_search_build(client, 0);
+ wanted_fields = 0;
+ if ((client->uidl_keymask & UIDL_MD5) != 0)
+ wanted_fields |= MAIL_FETCH_HEADER_MD5;
+
+ search_ctx = mailbox_search_init(client->trans, search_args,
+ NULL, wanted_fields, NULL);
+ mail_search_args_unref(&search_args);
+
+ uidl_duplicates_rename =
+ strcmp(client->set->pop3_uidl_duplicates, "rename") == 0;
+ if (uidl_duplicates_rename)
+ hash_table_create(&prev_uidls, default_pool, 0, str_hash,
+ strcmp);
+ client->uidl_pool = pool_alloconly_create("message uidls", 1024);
+
+ /* first read all the UIDLs into a temporary [seq] array */
+ seq_uidls = i_new(const char *, client->highest_seq);
+ str = t_str_new(128);
+ while (mailbox_search_next(search_ctx, &mail)) {
+ str_truncate(str, 0);
+ if (pop3_get_uid(client, mail, str, &permanent_uidl) < 0) {
+ failed = TRUE;
+ break;
+ }
+ if (uidl_duplicates_rename)
+ uidl_rename_duplicate(str, prev_uidls);
+
+ uidl = p_strdup(client->uidl_pool, str_c(str));
+ if (client->set->pop3_save_uidl && !permanent_uidl)
+ mail_update_pop3_uidl(mail, uidl);
+
+ i_assert(mail->seq <= client->highest_seq);
+ seq_uidls[mail->seq-1] = uidl;
+ if (uidl_duplicates_rename)
+ hash_table_update(prev_uidls, uidl, POINTER_CAST(1));
+ }
+ (void)mailbox_search_deinit(&search_ctx);
+ if (uidl_duplicates_rename)
+ hash_table_destroy(&prev_uidls);
+
+ if (failed) {
+ pool_unref(&client->uidl_pool);
+ i_free(seq_uidls);
+ return;
+ }
+ /* map UIDLs to msgnums (in case POP3 sort ordering is different) */
+ client->message_uidls = p_new(client->uidl_pool, const char *,
+ MALLOC_ADD(client->messages_count, 1));
+ for (msgnum = 0; msgnum < client->messages_count; msgnum++) {
+ client->message_uidls[msgnum] =
+ seq_uidls[msgnum_to_seq(client, msgnum) - 1];
+ }
+ i_free(seq_uidls);
+}
+
+static struct cmd_uidl_context *
+cmd_uidl_init(struct client *client, uint32_t seq)
+{
+ struct cmd_uidl_context *ctx;
+ struct mail_search_args *search_args;
+ enum mail_fetch_field wanted_fields;
+
+ if (client->message_uidls_save && client->message_uidls == NULL &&
+ client->messages_count > 0)
+ client_uidls_save(client);
+
+ ctx = i_new(struct cmd_uidl_context, 1);
+ ctx->list_all = seq == 0;
+
+ if (client->message_uidls == NULL) {
+ wanted_fields = 0;
+ if ((client->uidl_keymask & UIDL_MD5) != 0)
+ wanted_fields |= MAIL_FETCH_HEADER_MD5;
+
+ search_args = pop3_search_build(client, seq);
+ ctx->search_ctx = mailbox_search_init(client->trans, search_args,
+ pop3_sort_program,
+ wanted_fields, NULL);
+ mail_search_args_unref(&search_args);
+ }
+
+ if (seq == 0) {
+ client->cmd = cmd_uidl_callback;
+ client->cmd_context = ctx;
+ }
+ return ctx;
+}
+
+static int cmd_uidl(struct client *client, const char *args)
+{
+ struct cmd_uidl_context *ctx;
+ uint32_t seq;
+
+ if (*args == '\0') {
+ client_send_line(client, "+OK");
+ ctx = cmd_uidl_init(client, 0);
+ (void)list_uids_iter(client, ctx);
+ } else {
+ unsigned int msgnum;
+
+ if (get_msgnum(client, args, &msgnum, FALSE) == NULL)
+ return -1;
+
+ seq = msgnum_to_seq(client, msgnum);
+ ctx = cmd_uidl_init(client, seq);
+ ctx->msgnum = msgnum;
+ if (!list_uids_iter(client, ctx))
+ return client_reply_msg_expunged(client, msgnum);
+ }
+
+ return 1;
+}
+
+int client_command_execute(struct client *client,
+ const char *name, const char *args)
+{
+ /* keep the command uppercased */
+ name = t_str_ucase(name);
+
+ while (*args == ' ') args++;
+
+ switch (*name) {
+ case 'C':
+ if (strcmp(name, "CAPA") == 0)
+ return cmd_capa(client, args);
+ break;
+ case 'D':
+ if (strcmp(name, "DELE") == 0)
+ return cmd_dele(client, args);
+ break;
+ case 'L':
+ if (strcmp(name, "LIST") == 0)
+ return cmd_list(client, args);
+ if (strcmp(name, "LAST") == 0 && client->set->pop3_enable_last)
+ return cmd_last(client, args);
+ break;
+ case 'N':
+ if (strcmp(name, "NOOP") == 0)
+ return cmd_noop(client, args);
+ break;
+ case 'Q':
+ if (strcmp(name, "QUIT") == 0)
+ return cmd_quit(client, args);
+ break;
+ case 'R':
+ if (strcmp(name, "RETR") == 0)
+ return cmd_retr(client, args);
+ if (strcmp(name, "RSET") == 0)
+ return cmd_rset(client, args);
+ break;
+ case 'S':
+ if (strcmp(name, "STAT") == 0)
+ return cmd_stat(client, args);
+ break;
+ case 'T':
+ if (strcmp(name, "TOP") == 0)
+ return cmd_top(client, args);
+ break;
+ case 'U':
+ if (strcmp(name, "UIDL") == 0)
+ return cmd_uidl(client, args);
+ break;
+ }
+
+ client_send_line(client, "-ERR Unknown command: %s", name);
+ return -1;
+}