/* elmo - ELectronic Mail Operator Copyright (C) 2002, 2003, 2004 rzyjontko This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. ---------------------------------------------------------------------- This module implements RFC2821 and RFC821 (e)smtp client side protocol. Its implementation is based on the project stored in the diagram in smtp.dia. */ /**************************************************************************** * IMPLEMENTATION HEADERS ****************************************************************************/ #include #include #include #include #include "smtp.h" #include "networking.h" #include "error.h" #include "wrapbox.h" #include "mail.h" #include "mybox.h" #include "xmalloc.h" #include "ask.h" #include "gettext.h" #include "str.h" #include "debug.h" #include "file.h" #include "hmac-md5.h" /**************************************************************************** * IMPLEMENTATION PRIVATE DEFINITIONS / ENUMERATIONS / SIMPLE TYPEDEFS ****************************************************************************/ #ifndef HOST_NAME_MAX # define HOST_NAME_MAX 255 #endif #define TERMINATOR_RE "[0-9]{3,3} [^\r\n]*\r\n$" #define PICK_NEXT() do { conn.sent++; pick_message (); return; } while (0) enum state { SMTP_READY, /* ready to flush - initial / final state */ SMTP_DISCONNECTED, /* no connection */ SMTP_GREETING, /* after welcome message (greeting) */ SMTP_EHLO, /* after response to ehlo */ SMTP_HELO, /* after response to helo */ SMTP_AUTH, /* after response to auth */ SMTP_MAIL, /* after response to mail from */ SMTP_RCPT, /* after response to rcpt to */ SMTP_DATA, /* after response to data */ SMTP_QUIT, /* after response to quit */ }; enum auth { AUTH_NONE, /* authentication not supported */ AUTH_CRAM_MD5, /* CRAM-MD5 */ AUTH_PLAIN, /* PLAIN */ AUTH_LOGIN, /* LOGIN */ }; /**************************************************************************** * IMPLEMENTATION PRIVATE CLASS PROTOTYPES / EXTERNAL CLASS REFERENCES ****************************************************************************/ /**************************************************************************** * IMPLEMENTATION PRIVATE STRUCTURES / UTILITY CLASSES ****************************************************************************/ /**************************************************************************** * IMPLEMENTATION REQUIRED EXTERNAL REFERENCES (AVOID) ****************************************************************************/ /**************************************************************************** * IMPLEMENTATION PRIVATE DATA ****************************************************************************/ static char *auth_str[4] = { NULL, "CRAM-MD5", "PLAIN", "LOGIN", }; static struct { enum state state; /* connection state */ int nd; /* networking descriptor */ mail_array_t *marray; /* list of messages in outbox */ int sent; /* number of messages already sent */ int tried; /* number of recepients tried */ int accepted; /* number of recepients accepted */ mail_t *mail; /* a message to be sent */ address_t *recepient; /* message's recepient */ ask_t *ask; /* smtp_acc data */ str_t *send_buf; /* data sent to server */ str_t *error_message; /* error message displayed to the user */ enum auth auth; /* authentication method supported */ int ext_size; /* if server supports size extension */ int ext_pipeline; /* if server supports pipelining */ int ext_8bitmime; /* if server supports BODY=... */ int ext_auth; /* if server supports AUTH */ int max_size; /* maximal message size */ } conn; static char hostname[HOST_NAME_MAX + 1]; /**************************************************************************** * INTERFACE DATA ****************************************************************************/ /**************************************************************************** * IMPLEMENTATION PRIVATE FUNCTION PROTOTYPES ****************************************************************************/ static void pick_recepient (void); static void pick_message (void); static void pick_server (void); static void send_ehlo (void); static void send_helo (void); static void send_auth(void); static int send_mail_from (void); static void send_rcpt_to (void); static void send_data (void); static void send_quit (void); /**************************************************************************** * IMPLEMENTATION PRIVATE FUNCTIONS ****************************************************************************/ static void cleanup (int n) { if (conn.state != SMTP_QUIT || n == -1){ if (conn.marray) mail_array_destroy (conn.marray); if (conn.ask) ask_destroy (conn.ask); if (conn.send_buf) str_destroy (conn.send_buf); conn.mail = NULL; conn.marray = NULL; conn.ask = NULL; conn.send_buf = NULL; } if (conn.error_message) str_destroy (conn.error_message); conn.state = SMTP_READY; conn.nd = -1; conn.sent = 0; conn.tried = 0; conn.accepted = 0; conn.recepient = NULL; conn.error_message = NULL; conn.auth = AUTH_NONE; conn.ext_size = 0; conn.ext_pipeline = 0; conn.ext_8bitmime = 0; conn.max_size = 0; } static address_t * next_recepient (void) { int n = conn.tried; mail_t *mail = conn.mail; if (mail == NULL) return NULL; if (mail->to && n < mail->to->count){ return mail->to->array[n]; } else if (mail->to){ n -= mail->to->count; } if (mail->cc && n < mail->cc->count){ return mail->cc->array[n]; } else if (mail->cc){ n -= mail->cc->count; } if (mail->bcc && n < mail->bcc->count){ return mail->bcc->array[n]; } return NULL; } static void after_newline (char **buf, int *header) { switch (**buf){ case '.': str_put_string_len (conn.send_buf, "..", 2); break; case '\n': *header = 0; break; case 'B': if (*header){ if (strstr (*buf, "Bcc") == *buf){ while (**buf != '\n' && **buf) ++*buf; } if (**buf) ++*buf; } break; case 'X': if (*header){ if (strstr (*buf, "X-Elmo-SMTP") == *buf){ while (**buf != '\n' && **buf) ++*buf; } if (**buf) ++*buf; } break; } } static void copy_buf (char *buf) { int header = 1; char *s = buf; str_clear (conn.send_buf); while (*s){ switch (*s){ case '\n': str_put_string_len (conn.send_buf, "\r\n", 2); s++; after_newline (& s, & header); break; default: str_put_char (conn.send_buf, *s); s++; break; } } str_put_string_len (conn.send_buf, "\r\n.\r\n", 5); } static int load_file (const char *fname) { char *buf; int size; FILE *fp = fopen (fname, "r"); if (fp == NULL){ error_ (errno, "%s", fname); return 1; } if (file_whole (fp, & buf, & size)){ fclose (fp); error_ (errno, "%s", fname); return 1; } if (conn.send_buf->size < size * 2){ str_destroy (conn.send_buf); conn.send_buf = str_create_size (size * 2 + 5); } copy_buf (buf); xfree (buf); return 0; } static void parse_extension (char *s) { char *seek; if (strstr (s, "SIZE") == s || strstr (s, "size") == s){ conn.ext_size = 1; seek = s + 4; while (*seek && isspace (*seek)) seek++; if (isdigit (*seek)) conn.max_size = atoi (seek); return; } if (strcmp (s, "PIPELINING") == 0 || strcmp (s, "pipelining") == 0){ conn.ext_pipeline = 1; return; } if (strcmp (s, "8BITMIME") == 0 || strcmp (s, "8bitmime") == 0){ conn.ext_8bitmime = 1; return; } if (strstr (s, "AUTH") == s || strstr (s, "auth") == s){ conn.ext_auth = 1; if (strstr (s, "CRAM-MD5") || strstr (s, "cram-md5")){ conn.auth = AUTH_CRAM_MD5; } else if (strstr (s, "LOGIN") || strstr (s, "login")){ conn.auth = AUTH_LOGIN; } else if (strstr (s, "PLAIN") || strstr (s, "plain")){ conn.auth = AUTH_PLAIN; } else { conn.auth = AUTH_NONE; } return; } } static void parse_extensions (char *msg) { int i; rstring_t *exts; exts = rstring_split_re (msg, "\r\n([0-9]{3,3}[ \\-])?"); if (exts == NULL) return; for (i = 1; i < exts->count; i++){ parse_extension (exts->array[i]); } rstring_delete (exts); } static int parse_answer (char *msg, int d) { char *seek; if (*msg == d) return 0; if (conn.error_message) str_destroy (conn.error_message); seek = strchr (msg, '\r'); if (seek) *seek = '\0'; conn.error_message = str_dup (msg + 4); if (seek) *seek = '\r'; return 1; } static void report_error (const char *str) { if (conn.error_message){ error_ (0, "%s (%s)", str, conn.error_message->str); } else { error_ (0, "%s", str); } } static void s_sent (char *msg, int len) { char *sent = mybox_sent (); if (parse_answer (msg, '2')){ report_error (_("couldn't transmit message")); } conn.mail->flags &= ~ FLAG_READ; if (wrapbox_move_mail_to (conn.mail, sent)){ error_ (0, _("couldn't move mail to %s"), sent); } xfree (sent); PICK_NEXT (); } static void s_data (char *msg, int len) { char *fname; if (conn.state != SMTP_DATA){ debug_msg (DEBUG_ERROR, "invalid state in s_data"); return; } if (parse_answer (msg, '3')){ report_error (_("couldn't transmit message")); PICK_NEXT (); } fname = wrapbox_fetch_single (conn.mail); if (load_file (fname)){ xfree (fname); PICK_NEXT (); } xfree (fname); net_send_progress (conn.nd, _("sending message %d"), conn.sent + 1); net_combo (conn.nd, conn.send_buf->str, conn.send_buf->len, TERMINATOR_RE, s_sent); } static void s_rcpt_to (char *msg, int len) { if (conn.state != SMTP_RCPT){ debug_msg (DEBUG_ERROR, "invalid state in s_rcpt_to"); return; } if (parse_answer (msg, '2')){ report_error (_("recepient address was rejected")); } else { conn.accepted++; } conn.tried++; pick_recepient (); } static void s_mail_from (char *msg, int len) { if (conn.state != SMTP_MAIL){ debug_msg (DEBUG_ERROR, "invalid state in s_mail_from"); return; } if (parse_answer (msg, '2')){ report_error (_("your email address was rejected")); PICK_NEXT (); } pick_recepient (); } static void s_ehlo (char *msg, int len) { char *user; char *pass; if (conn.state != SMTP_EHLO){ debug_msg (DEBUG_ERROR, "invalid state in s_ehlo"); return; } if (parse_answer (msg, '2')){ send_helo (); return; } parse_extensions (msg); if (conn.ext_auth && conn.auth != AUTH_NONE){ user = ask_get_field (conn.ask, "username"); pass = ask_get_field (conn.ask, "password"); if (user && pass){ send_auth (); return; } } send_mail_from (); } static void s_helo (char *msg, int len) { if (conn.state != SMTP_HELO){ debug_msg (DEBUG_ERROR, "invalid state in s_helo"); return; } if (parse_answer (msg, '2')){ PICK_NEXT (); } send_mail_from (); } static void s_auth_final (char *msg, int len) { if (parse_answer (msg, '2')){ report_error (_("authorization failed")); PICK_NEXT (); } send_mail_from (); } static void s_auth_cram_md5 (char *msg, int len) { int tmp_len; char *tmp; char *user; char *pass; char digest[16]; char *seek; str_t *challenge; str_t *answer; str_t *result; mime_t mime; int pass_len; int user_len; int i; seek = strchr (msg, ' '); if (seek == NULL){ error_ (0, "%s", _("invalid challenge from server")); PICK_NEXT (); } else seek++; mime.off_start = 0; mime.off_end = len - (seek - msg); mime.encoding = MENC_BASE64; mime.charset = NULL; challenge = mime_decode (& mime, seek, 0); user = ask_get_field (conn.ask, "username"); pass = ask_get_field (conn.ask, "password"); pass_len = strlen (pass); user_len = strlen (user); hmac_md5 (challenge->str, challenge->len, pass, pass_len, digest); answer = str_create_size (user_len + 1 + 32 + 1); str_sprintf (answer, "%s ", user); for (i = 0; i < 16; i++) str_sprintf (answer, "%02x", (unsigned char) digest[i]); tmp_len = answer->len; tmp = str_finished (answer); result = mime_encode (& mime, tmp, tmp_len); str_clear (conn.send_buf); str_sprintf (conn.send_buf, "%s\r\n", result->str); str_destroy (result); str_destroy (challenge); net_combo (conn.nd, conn.send_buf->str, conn.send_buf->len, TERMINATOR_RE, s_auth_final); } static void s_auth_plain (char *msg, int len) { char *user; char *pass; char *answer; str_t *encoded; mime_t mime; int pass_len; int user_len; int s_len; user = ask_get_field (conn.ask, "username"); pass = ask_get_field (conn.ask, "password"); user_len = strlen (user); pass_len = strlen (pass); s_len = 1 + user_len + 1 + pass_len + 1; answer = xmalloc (s_len); sprintf (answer, "%c%s%c%s", '\0', user, '\0', pass); mime.encoding = MENC_BASE64; encoded = mime_encode (& mime, answer, s_len - 1); str_clear (conn.send_buf); str_sprintf (conn.send_buf, "%s\r\n", encoded->str); str_destroy (encoded); net_combo (conn.nd, conn.send_buf->str, conn.send_buf->len, TERMINATOR_RE, s_auth_final); } static void s_auth_login_2 (char *msg, int len) { char *pass; str_t *encoded; mime_t mime; int pass_len; if (parse_answer (msg, '3')){ report_error (_("authorization failed")); PICK_NEXT (); } pass = xstrdup (ask_get_field (conn.ask, "password")); pass_len = strlen (pass); mime.encoding = MENC_BASE64; encoded = mime_encode (& mime, pass, pass_len); str_clear (conn.send_buf); str_sprintf (conn.send_buf, "%s\r\n", encoded->str); str_destroy (encoded); net_combo (conn.nd, conn.send_buf->str, conn.send_buf->len, TERMINATOR_RE, s_auth_final); } static void s_auth_login (char *msg, int len) { char *user; str_t *encoded; mime_t mime; int user_len; user = xstrdup (ask_get_field (conn.ask, "username")); user_len = strlen (user); mime.encoding = MENC_BASE64; encoded = mime_encode (& mime, user, user_len); str_clear (conn.send_buf); str_sprintf (conn.send_buf, "%s\r\n", encoded->str); str_destroy (encoded); net_combo (conn.nd, conn.send_buf->str, conn.send_buf->len, TERMINATOR_RE, s_auth_login_2); } static void s_auth (char *msg, int len) { if (parse_answer (msg, '3')){ report_error (_("authorization request failed")); PICK_NEXT (); } switch (conn.auth){ case AUTH_NONE: break; case AUTH_CRAM_MD5: s_auth_cram_md5 (msg, len); break; case AUTH_PLAIN: s_auth_plain (msg, len); break; case AUTH_LOGIN: s_auth_login (msg, len); break; } } static void s_greeting (char *msg, int len) { if (conn.state != SMTP_GREETING){ debug_msg (DEBUG_ERROR, "invalid state in s_greeting"); return; } if (parse_answer (msg, '2')){ report_error (_("connection rejected")); PICK_NEXT (); } send_ehlo (); } static void s_disconnected (void) { int secure; int port; char *host; if (conn.state != SMTP_DISCONNECTED){ debug_msg (DEBUG_ERROR, "invalid state in s_disconnected"); return; } if (conn.mail == NULL){ conn.state = SMTP_READY; return; } host = ask_get_field (conn.ask, "server"); secure = ask_get_field_int_default (conn.ask, "ssl", 0); port = ask_get_field_int_default (conn.ask, "port", (secure) ? 465 : 25); if (host == NULL){ error_ (0, "%s", _("server field in smtp_acc not defined")); PICK_NEXT (); } conn.nd = net_open (host, port, secure, cleanup); if (conn.nd == -1){ PICK_NEXT (); } conn.state = SMTP_GREETING; net_recv_data (conn.nd, TERMINATOR_RE, s_greeting); } static void s_quit (char *msg, int len) { if (conn.state != SMTP_QUIT){ debug_msg (DEBUG_ERROR, "invalid state in s_quit"); return; } if (parse_answer (msg, '2')){ report_error (_("does your server love you so much?")); } net_close (conn.nd); conn.state = SMTP_DISCONNECTED; s_disconnected (); } static void pick_recepient (void) { conn.recepient = next_recepient (); if (conn.recepient) send_rcpt_to (); else if (conn.accepted > 0) send_data (); else PICK_NEXT (); } static void pick_server (void) { char *name; if (conn.ask == NULL){ error_ (0, "%s", _("no smtp_acc defined")); return; } name = ask_get_field (conn.ask, "name"); if (name == NULL){ error_ (0, "%s", _("name field in smtp_acc not defined")); return; } if (conn.mail->smtp == NULL){ if (conn.mail->subject) error_ (0, _("no smtp for message %d (subject: %s)"), conn.sent + 1, conn.mail->subject); else error_ (0, _("no smtp for message %d"), conn.sent + 1); PICK_NEXT (); } if (conn.nd == -1){ ask_change_where (conn.ask, "name", conn.mail->smtp); conn.state = SMTP_DISCONNECTED; s_disconnected (); return; } if (strcmp (conn.mail->smtp, name) == 0){ if (send_mail_from ()){ PICK_NEXT (); } return; } send_quit (); } static void pick_message (void) { conn.tried = 0; conn.accepted = 0; conn.recepient = NULL; conn.mail = mail_array_get (conn.marray, conn.sent); if (conn.mail == NULL && conn.nd == -1) return; if (conn.mail == NULL){ send_quit (); return; } pick_server (); } static void send_ehlo (void) { conn.state = SMTP_EHLO; str_clear (conn.send_buf); str_sprintf (conn.send_buf, "EHLO %s\r\n", hostname); net_combo (conn.nd, conn.send_buf->str, conn.send_buf->len, TERMINATOR_RE, s_ehlo); } static void send_helo (void) { conn.state = SMTP_HELO; str_clear (conn.send_buf); str_sprintf (conn.send_buf, "HELO %s\r\n", hostname); net_combo (conn.nd, conn.send_buf->str, conn.send_buf->len, TERMINATOR_RE, s_helo); } static void send_auth (void) { conn.state = SMTP_AUTH; str_clear (conn.send_buf); str_sprintf (conn.send_buf, "AUTH %s\r\n", auth_str[conn.auth]); net_combo (conn.nd, conn.send_buf->str, conn.send_buf->len, TERMINATOR_RE, s_auth); } static int send_mail_from (void) { char *email; email = ask_get_field (conn.ask, "email"); if (email == NULL){ error_ (0, "%s", _("email field in smtp_acc not defined")); return 1; } conn.state = SMTP_MAIL; str_clear (conn.send_buf); str_sprintf (conn.send_buf, "MAIL FROM:<%s>\r\n", email); net_combo (conn.nd, conn.send_buf->str, conn.send_buf->len, TERMINATOR_RE, s_mail_from); return 0; } static void send_rcpt_to (void) { conn.state = SMTP_RCPT; str_clear (conn.send_buf); str_sprintf (conn.send_buf, "RCPT TO:<%s>\r\n", conn.recepient->email); net_combo (conn.nd, conn.send_buf->str, conn.send_buf->len, TERMINATOR_RE, s_rcpt_to); } static void send_data (void) { conn.state = SMTP_DATA; net_combo (conn.nd, "DATA\r\n", 6, TERMINATOR_RE, s_data); } static void send_quit (void) { conn.state = SMTP_QUIT; net_combo (conn.nd, "QUIT\r\n", 6, TERMINATOR_RE, s_quit); } /**************************************************************************** * INTERFACE FUNCTIONS ****************************************************************************/ void smtp_init (void) { conn.state = SMTP_READY; conn.nd = -1; conn.marray = NULL; conn.sent = 0; conn.tried = 0; conn.accepted = 0; conn.mail = NULL; conn.recepient = NULL; conn.ask = NULL; conn.send_buf = NULL; conn.error_message = NULL; conn.ext_size = 0; conn.ext_pipeline = 0; conn.ext_8bitmime = 0; conn.max_size = 0; gethostname (hostname, HOST_NAME_MAX); hostname[HOST_NAME_MAX] = '\0'; } void smtp_free_resources (void) { if (conn.nd != -1) net_close (conn.nd); else cleanup (-1); } void smtp_flush_outbox (void) { char *outbox; if (conn.state != SMTP_READY || conn.nd != -1){ error_ (0, "%s", _("connection in progress")); return; } outbox = mybox_outbox (); if (conn.marray) mail_array_destroy (conn.marray); conn.marray = wrapbox_open_box (outbox); if (conn.marray == NULL){ error_ (0, _("couldn't open %s"), outbox); xfree (outbox); return; } xfree (outbox); mail_array_sort_smtp (conn.marray); if (conn.ask) ask_destroy (conn.ask); conn.ask = ask_select_default ("smtp_acc"); conn.mail = NULL; conn.sent = 0; conn.send_buf = str_create (); pick_message (); } /**************************************************************************** * INTERFACE CLASS BODIES ****************************************************************************/ /**************************************************************************** * * END MODULE smtp.c * ****************************************************************************/