/** * @brief Initialize a TLS session for an unauthenticated POP3 session. * @note RFC 2595 / section 4 dictates that the STLS/STARTTLS command should only be available in the authorization state. * @param con the connection of the POP3 client requesting the transport layer security upgrade. * @return This function returns no value (all error messages are written directly to the requesting client). */ void pop_starttls(connection_t *con) { if (con->pop.session_state != 0) { pop_invalid(con); return; } else if (con_secure(con) == 1) { con_write_bl(con, "-ERR Session is already encrypted.\r\n", 36); return; } else if (con_secure(con) == -1) { con_write_bl(con, "-ERR This server has not been configured to support STLS.\r\n", 59); return; } // Tell the user that we are ready to start the negotiation. con_write_bl(con, "+OK Ready to start TLS negotiation.\r\n", 37); if (!(con->network.ssl = ssl_alloc(con->server, con->network.sockd, M_SSL_BIO_NOCLOSE))) { con_write_bl(con, "-ERR STARTTLS FAILED\r\n", 22); log_pedantic("The SSL connection attempt failed."); return; } stats_increment_by_name("pop.connections.secure"); st_length_set(con->network.buffer, 0); con->network.line = pl_null(); con->network.status = 1; pop_session_reset(con); return; }
/** * @brief Get a message, in response to a POP3 DELE command. * @param con the POP3 client connection issuing the command. * @return This function returns no value. */ void pop_dele(connection_t *con) { uint64_t number; meta_message_t *active = NULL; if (con->pop.session_state != 1) { pop_invalid(con); return; } // A message number is a required argument. if (!pop_num_parse(con, &number, true)) { con_write_bl(con, "-ERR The delete command requires a numeric argument.\r\n", 54); return; } meta_user_wlock(con->pop.user); if (!(active = pop_get_message(con->pop.user->messages, number))) { con_write_bl(con, "-ERR Message not found.\r\n", 25); } else if ((active->status & MAIL_STATUS_HIDDEN) == MAIL_STATUS_HIDDEN) { con_write_bl(con, "-ERR Message already deleted.\r\n", 31); } else { active->status += MAIL_STATUS_HIDDEN; con_write_bl(con, "+OK Message marked for deletion.\r\n", 34); } meta_user_unlock(con->pop.user); return; }
/** * @brief Accept a username for POP3 authentication. * @note This command is only allowed for sessions which have not yet been authenticated. * If the username has already been supplied pre-authentication, the old value will be overwritten with the new one. * @param con the POP3 client connection issuing the command. * @brief This function returns no value. */ void pop_user(connection_t *con) { stringer_t *username, *clean; if (con->pop.session_state != 0) { pop_invalid(con); return; } // If they didn't pass in a valid username. if (!(username = pop_user_parse(con)) || !(clean = credential_address(username))) { con_write_bl(con, "-ERR Invalid USER command.\r\n", 28); st_cleanup(username); return; } // Check for a previously provided value and free it. st_cleanup(con->pop.username); st_free(username); // Store the value we were given. Until authentication, this will be the fully qualified username. con->pop.username = clean; // Tell the client everything worked. con_write_bl(con, "+OK Username accepted.\r\n", 24); return; }
/** * @brief Get the UIDL for a message or collection of messages, in response to a POP3 UIDL command. * @param con the POP3 client connection issuing the command. * @return This function returns no value. */ void pop_uidl(connection_t *con) { uint64_t number; inx_cursor_t *cursor; meta_message_t *active; bool_t result; if (con->pop.session_state != 1) { pop_invalid(con); return; } // Get the argument, if any. result = pop_num_parse(con, &number, false); meta_user_rlock(con->pop.user); // Output all of the messages that aren't deleted or appended. if (!result) { number = 1; con_print(con, "+OK %llu messages total.\r\n", pop_total_messages(con->pop.user->messages)); if (con->pop.user->messages && (cursor = inx_cursor_alloc(con->pop.user->messages))) { while ((active = inx_cursor_value_next(cursor))) { if ((active->status & (MAIL_STATUS_APPENDED | MAIL_STATUS_HIDDEN)) == 0) { con_print(con, "%llu %llu\r\n", number++, active->messagenum); } else if ((active->status & MAIL_STATUS_APPENDED) == 0) { number++; } } inx_cursor_free(cursor); } con_write_bl(con, ".\r\n", 3); } // Output a specific message. else { if (!(active = pop_get_message(con->pop.user->messages, number))) { con_write_bl(con, "-ERR Message not found.\r\n", 25); } else if ((active->status & MAIL_STATUS_HIDDEN) == MAIL_STATUS_HIDDEN) { con_write_bl(con, "-ERR Message marked for deletion.\r\n", 35); } else { con_print(con, "+OK %llu %llu\r\n", number, active->messagenum); } } meta_user_unlock(con->pop.user); return; }
/** * @brief Initialize a new POP3 connection. * @param con the newly connected POP3 client connection. * @return This function returns no value. */ void pop_init(connection_t *con) { con_write_bl(con, "+OK magma\r\n", 11); pop_requeue(con); return; }
/** * @brief Gracefully destroy a POP3 session, whether because of an error or in response to a user QUIT command. * @param con the POP3 client connection to be shut down. * @brief This function returns no value. */ void pop_quit(connection_t *con) { if (con_status(con) == 2) { con_write_bl(con, "-ERR Unexpected connection shutdown detected. Goodbye.\r\n", 56); } else if (con_status(con) >= 0) { con_write_bl(con, "+OK Goodbye.\r\n", 14); } else { con_write_bl(con, "-ERR Network connection failure.\r\n", 34); } con_destroy(con); return; }
void molten_stats(connection_t *con) { size_t length; length = stats_get_count(); for(size_t i = 0; i < length; i++) { if (con_print(con, "STAT %s %lu\r\n", stats_get_name(i), stats_get_value_by_num(i)) < 0) { enqueue(&molten_quit, con); return; } } length = stats_derived_count(); for(size_t i = 0; i < length; i++) { if (con_print(con, "STAT %s %lu\r\n", stats_derived_name(i), stats_derived_value(i)) < 0) { enqueue(&molten_quit, con); return; } } con_write_bl(con, "END\r\n", 5) < 0 ? enqueue(&molten_quit, con) : enqueue(&molten_parse, con); return; }
/** * @brief A function handler for invalid POP3 commands. * @param con the POP3 client connection issuing the command. * @return This function returns no value. */ void pop_invalid(connection_t *con) { con->protocol.violations++; usleep(con->server->violations.delay); con_write_bl(con, "-ERR Unrecognized command.\r\n", 28); return; }
/** * @brief Reset the user's mailbox, in response to a POP3 RSET command. * @see pop_session_reset() * @param con the POP3 client connection issuing the command. * @return This function returns no value. */ void pop_rset(connection_t *con) { int_t ret; if (con->pop.session_state != 1) { pop_invalid(con); } else if ((ret = pop_session_reset(con)) == 1) { con_write_bl(con, "+OK All messages were reset.\r\n", 30); } else if (ret == -1) { con_write_bl(con, "-ERR Message reset failed.\r\n", 28); } else { con_write_bl(con, "-ERR Session reset complete.\r\n", 30); } return; }
/** * @brief Retrieve a user's message, in response to a POP3 RETR command. * @note This function will fail if a deleted message was specified by the user. * @param con the POP3 client connection issuing the command. * @return This function returns no value. */ void pop_retr(connection_t *con) { uint64_t number; meta_message_t *meta; mail_message_t *message; if (con->pop.session_state != 1) { pop_invalid(con); return; } // Which message are we getting. if (!pop_num_parse(con, &number, true)) { con_write_bl(con, "-ERR The retrieve command requires a numeric argument.\r\n", 56); return; } meta_user_rlock(con->pop.user); // Get the message. if (!(meta = pop_get_message(con->pop.user->messages, number))) { meta_user_unlock(con->pop.user); con_write_bl(con, "-ERR Message not found.\r\n", 25); return; } // Check for deletion. if ((meta->status & MAIL_STATUS_HIDDEN) == MAIL_STATUS_HIDDEN) { meta_user_unlock(con->pop.user); con_write_bl(con, "-ERR This message has been marked for deletion.\r\n", 49); return; } // Load the message and spit back the right number of lines. if (!(message = mail_load_message(meta, con->pop.user, con->server, 1))) { meta_user_unlock(con->pop.user); con_write_bl(con, "-ERR The message you requested could not be loaded into memory. It has either been " "deleted by another connection or is corrupted.\r\n", 131); return; } meta_user_unlock(con->pop.user); // Dot stuff the message. st_replace(&(message->text), PLACER("\n.", 2), PLACER("\n..", 3)); // Tell the client to prepare for a message. The size is strictly informational. con_print(con, "+OK %u characters follow.\r\n", st_length_get(message->text)); // We use raw socket IO because it is much faster when writing large amounts of data. con_write_st(con, message->text); // If the message didn't end with a line break, spit two. if (*(st_char_get(message->text) + st_length_get(message->text) - 1) == '\n') { con_write_bl(con, ".\r\n", 3); } else { con_write_bl(con, "\r\n.\r\n", 5); } mail_destroy(message); return; }
/** * @brief Execute a POP3 no-operation command. * @param con the POP3 client connection issuing the command. * @return This function returns no value. */ void pop_noop(connection_t *con) { con_write_bl(con, "+OK\r\n", 5); return; }
/** * @brief Accept and verify a password for POP3 authentication. * @note This command is only allowed for sessions which have not yet been authenticated, but which have already supplied a username. * If the username/password combo was validated, the account information is retrieved and checked to see if it is locked. * After successful authentication, this function will prohibit insecure connections for any user configured to use SSL only, * and enforce the existence of only one POP3 session at a time. * Finally, the database Log table for this user's POP3 access is updated, and all the user's messages are retrieved. * @param con the POP3 client connection issuing the command. * @return This function returns no value. */ void pop_pass(connection_t *con) { int_t state; credential_t *cred; stringer_t *password, *username; if (con->pop.session_state != 0) { pop_invalid(con); return; } // The user must come before the PASS command. if (st_empty(con->pop.username)) { con_write_bl(con, "-ERR You must supply a username first.\r\n", 40); return; } // If they didn't pass in a valid password. if (!(password = pop_pass_parse(con))) { con_write_bl(con, "-ERR Invalid PASS command.\r\n", 28); return; } // Hash the password. // First we need to get the regular username from the fully qualified one. if (!(username = credential_username(con->pop.username))) { con_write_bl(con, "-ERR Internal server error. Please try again later.\r\n", 53); st_wipe(password); st_free(password); } st_free(con->pop.username); con->pop.username = username; if (!(cred = credential_alloc_auth(con->pop.username, password))) { con_write_bl(con, "-ERR Internal server error. Please try again later.\r\n", 53); st_wipe(password); st_free(password); return; } st_wipe(password); st_free(password); // Pull the user info out. state = meta_get(con->pop.username, cred->auth.domain, cred->auth.password, cred->auth.key, META_PROT_POP, META_GET_MESSAGES, &(con->pop.user)); // Securely delete this information, as these are the keys to the castle. credential_free(cred); // Not found, or invalid password. if (state == 0) { con_write_bl(con, "-ERR The username and password combination is invalid.\r\n", 56); return; } // Internal error. else if (state < 0 || !con->pop.user) { con_write_bl(con, "-ERR [SYS/TEMP] Internal server error. Please try again later.\r\n", 64); return; } // Locks else if (con->pop.user->lock_status != 0) { // What type of lock is it. if (con->pop.user->lock_status == 1) { con_write_bl(con, "-ERR [SYS/PERM] This account has been administratively locked.\r\n", 64); } else if (con->pop.user->lock_status == 2) { con_write_bl(con, "-ERR [SYS/PERM] This account has been locked for inactivity.\r\n", 62); } else if (con->pop.user->lock_status == 3) { con_write_bl(con, "-ERR [SYS/PERM] This account has been locked on suspicion of abuse.\r\n", 69); } else if (con->pop.user->lock_status == 4) { con_write_bl(con, "-ERR [SYS/PERM] This account has been locked at the request of the user.\r\n", 74); } else { con_write_bl(con, "-ERR [SYS/PERM] This account has been locked.\r\n", 47); } con->pop.user = NULL; meta_remove(con->pop.username, META_PROT_POP); return; } // SSL check. else if ((con->pop.user->flags & META_USER_SSL) == META_USER_SSL && con_secure(con) != 1) { con->pop.user = NULL; meta_remove(con->pop.username, META_PROT_POP); con_write_bl(con, "-ERR [SYS/PERM] This user account is configured to require that all POP sessions be connected over SSL.\r\n", 105); return; } // Single session check. else if (con->pop.user->refs.pop != 1) { con->pop.user = NULL; meta_remove(con->pop.username, META_PROT_POP); con_write_bl(con, "-ERR [IN-USE] This account is being used by another session. Please try again in a few minutes.\r\n", 97); return; } // Debug logging. log_pedantic("User %.*s logged in from %s via POP. {poprefs = %lu, imaprefs = %lu, messages = %lu}", st_length_int(con->pop.username), st_char_get(con->pop.username), st_char_get(con_addr_presentation(con, MANAGEDBUF(256))), con->pop.user->refs.pop, con->pop.user->refs.imap, con->pop.user->messages ? inx_count(con->pop.user->messages) : 0); // Update the log and unlock the session. meta_data_update_log(con->pop.user, META_PROT_POP); meta_user_wlock(con->pop.user); meta_messages_login_update(con->pop.user, META_LOCKED); meta_user_unlock(con->pop.user); // Update session state. con->pop.session_state = 1; // Tell the client everything worked. con_write_bl(con, "+OK Password accepted.\r\n", 24); return; }
/** * @brief Perform client command processing on an established imap session. * @note This function will read the next line of user input, parse the command, and then attempt to execute it with the appropriate handler. * @param con a pointer to the connection object underlying the imap session. * @return This function returns no value. */ void imap_process(connection_t *con) { int_t state; command_t *command, client = { .function = NULL }; // If the connection indicates an error occurred, or the socket was closed by the client we send the connection to the logout function. if (((state = con_read_line(con, false)) < 0) || (state == -2)) { con->command = NULL; enqueue(&imap_logout, con); return; } else if (pl_empty(con->network.line) && ((con->protocol.spins++) + con->protocol.violations) > con->server->violations.cutoff) { con->command = NULL; enqueue(&imap_logout, con); return; } else if (pl_empty(con->network.line)) { con->command = NULL; enqueue(&imap_process, con); return; } // Parse the line into its tag and command elements. if ((state = imap_command_parser(con)) < 0) { // Try to be helpful about the parsing error. if (state == -1) { con_write_bl(con, "* BAD Unable to parse the command tag.\r\n", 40); } else if (state == -2) { con_print(con, "%.*s BAD Unable to parse the command.\r\n", st_length_int(con->imap.tag), st_char_get(con->imap.tag)); } else { con_print(con, "%.*s BAD The command arguments were submitted using an invalid syntax.\r\n", st_length_int(con->imap.tag), st_char_get(con->imap.tag)); } // If the client keeps breaking rules drop them. if (((con->protocol.spins++) + con->protocol.violations) > con->server->violations.cutoff) { con->command = NULL; enqueue(&imap_logout, con); return; } // Requeue and hope the next line of data is useful. con->command = NULL; enqueue(&imap_process, con); return; } client.string = st_char_get(con->imap.command); client.length = st_length_get(con->imap.command); if ((command = bsearch(&client, imap_commands, sizeof(imap_commands) / sizeof(imap_commands[0]), sizeof(command_t), imap_compare))) { con->command = command; con->protocol.spins = 0; if (command->function == &imap_logout) { enqueue(command->function, con); } else { requeue(command->function, &imap_requeue, con); } } else { con->command = NULL; requeue(&imap_invalid, &imap_requeue, con); } return; }
/** * @brief Extract the contents of a literal string and advance the position of the parser stream. * @note This function expects as input a string beginning with '{' and followed by a numerical string, an optional '+', and a closing '}'. After reading in the numerical size parameter, it then attempts to read in that many bytes of input from the network stream. * @param con the client IMAP connection passing the literal string as input to the server. * @param output the address of a managed string that will receive a copy of the literal string's contents on success, or NULL on failure or if it is zero length. * @param start the address of a pointer to the start of the buffer to be parsed (beginning with '{'), that will also be updated to * point to the next argument in the sequence on success. * @param length a pointer to a size_t variable that contains the length of the string to be parsed, and that will be updated to reflect * the length of the remainder of the input string that follows the parsed literal string. * @return -1 on general or parse error or if an enclosing pair of double quotes was not found, or 1 if the supplied quoted string was valid. */ int_t imap_parse_literal(connection_t *con, stringer_t **output, chr_t **start, size_t *length) { chr_t *holder; int_t plus = 0; stringer_t *result; size_t characters, left; ssize_t nread; uint64_t literal, number; // Get setup. holder = *start; left = *length; *output = NULL; // Skip the opening bracket. if (*holder != '{' || !left) { return -1; } else { holder++; left--; } // Advance until we have a break character. while (left && *holder >= '0' && *holder <= '9') { holder++; left--; } // Store the length. characters = holder - *start - 1; if (left && *holder == '+') { plus = 1; holder++; left--; } if (*holder != '}' || !characters) { return -1; } // Convert to a number. Make sure the number is positive. if (!uint64_conv_bl(*start + 1, characters, &number)) { return -1; } literal = (size_t)number; // If the number is larger than 128 megabytes, then reject it. if (!plus && number > 134217728) { return -1; } // They client is already transmitting, so read the entire file, then reject it. else if (number > 134217728) { while (number > 0) { // Read the data. if ((nread = con_read(con)) <= 0) { log_pedantic("The connection was dropped while reading the literal."); return -1; } // Deal with signedness problem. characters = nread; if (number > (uint64_t)characters) { number -= characters; } else { // If we have any extra characters in the buffer, move them to the beginning. if ((uint64_t)characters > number) { mm_move(st_char_get(con->network.buffer), st_char_get(con->network.buffer) + number, characters - number); st_length_set(con->network.buffer, characters - number); con->network.line = line_pl_st(con->network.buffer, 0); } else { st_length_set(con->network.buffer, 0); con->network.line = pl_null(); } // Make sure we have a full line. if (pl_empty(con->network.line) && con_read_line(con, true) <= 0) { log_pedantic("The connection was dropped while reading the literal."); return -1; } number = 0; } } return -1; } // If this is not a plus literal, output the proceed statement. if (!plus) { con_write_bl(con, "+ GO\r\n", 6); } // Handle the special case of a zero length literal. if (literal == 0) { // Read the next line. if (con_read_line(con, true) <= 0) { log_pedantic("The connection was dropped while reading the literal."); return -1; } *start = st_char_get(con->network.buffer); *length = pl_length_get(con->network.line); // There should be a space before the next argument. if (*length && **start == ' ') { (*start)++; (*length)--; } return 1; } // Allocate a stringer for the buffer. if (!(result = st_alloc(literal))) { log_pedantic("Unable to allocate a buffer of %lu bytes for the literal argument.", literal); return -1; } // So we know how many more characters to read. left = literal; // Where we put the data. holder = st_char_get(result); // Keep looping until we run out of data. while (left) { // Read the data. if ((nread = con_read(con)) <= 0) { log_pedantic("The connection was dropped while reading the literal."); st_free(result); return -1; } characters = nread; // If we have a buffer, copy the data into the buffer. mm_copy(holder, st_char_get(con->network.buffer), (left > characters) ? characters : left); if (left > characters) { holder += characters; left -= characters; } else { st_length_set(result, literal); // If we have any extra characters in the buffer, move them to the beginning. if (characters > left) { mm_move(st_char_get(con->network.buffer), st_char_get(con->network.buffer) + left, characters - left); st_length_set(con->network.buffer, characters - left); con->network.line = line_pl_st(con->network.buffer, 0); } else { st_length_set(con->network.buffer, 0); con->network.line = pl_null(); } // Make sure we have a full line. if (pl_empty(con->network.line) && con_read_line(con, true) <= 0) { log_pedantic("The connection was dropped while reading the literal."); st_free(result); return -1; } left = 0; } } *start = st_char_get(con->network.buffer); *length = pl_length_get(con->network.line); // There should be a space before the next argument. if (*length && **start == ' ') { (*start)++; (*length)--; } if (result != NULL) { *output = result; } else { return -1; } return 1; }
void molten_invalid(connection_t *con) { con_write_bl(con, "ERROR\r\n", 7) < 0 ? enqueue(&molten_quit, con) : enqueue(&molten_parse, con); return; }