/*
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 <unistd.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#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
*
****************************************************************************/
syntax highlighted by Code2HTML, v. 0.9.1