isync/imap.c
Michael Elkins acd674f93e allow leading whitespace in config files
now possible to sync multiple mailboxes by specifying multiple aliases on
the command line.  IMAP connections are reused if possible.

don't initialize ssl unless we are going to use it.
2000-12-27 21:14:22 +00:00

906 lines
18 KiB
C

/* $Id$
*
* isync - IMAP4 to maildir mailbox synchronizer
* Copyright (C) 2000 Michael R. Elkins <me@mutt.org>
*
* 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; either version 2 of the License, or
* (at your option) any later version.
*
* 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
*/
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <ctype.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#if HAVE_LIBSSL
#include <openssl/err.h>
#endif
#include "isync.h"
const char *Flags[] = {
"\\Seen",
"\\Answered",
"\\Deleted",
"\\Flagged",
"\\Recent",
"\\Draft"
};
#if HAVE_LIBSSL
#define MAX_DEPTH 1
SSL_CTX *SSLContext = 0;
void
free_message (message_t * msg)
{
message_t *tmp;
while (msg)
{
tmp = msg;
msg = msg->next;
if (tmp->file)
free (tmp->file);
free (tmp);
}
}
/* this gets called when a certificate is to be verified */
static int
verify_cert (SSL * ssl)
{
X509 *cert;
int err;
char buf[256];
int ret = -1;
BIO *bio;
cert = SSL_get_peer_certificate (ssl);
if (!cert)
{
puts ("Error, no server certificate");
return -1;
}
err = SSL_get_verify_result (ssl);
if (err == X509_V_OK)
return 0;
printf ("Error, can't verify certificate: %s (%d)\n",
X509_verify_cert_error_string (err), err);
X509_NAME_oneline (X509_get_subject_name (cert), buf, sizeof (buf));
printf ("\nSubject: %s\n", buf);
X509_NAME_oneline (X509_get_issuer_name (cert), buf, sizeof (buf));
printf ("Issuer: %s\n", buf);
bio = BIO_new (BIO_s_mem ());
ASN1_TIME_print (bio, X509_get_notBefore (cert));
memset (buf, 0, sizeof (buf));
BIO_read (bio, buf, sizeof (buf) - 1);
printf ("Valid from: %s\n", buf);
ASN1_TIME_print (bio, X509_get_notAfter (cert));
memset (buf, 0, sizeof (buf));
BIO_read (bio, buf, sizeof (buf) - 1);
BIO_free (bio);
printf (" to: %s\n", buf);
printf
("\n*** WARNING *** There is no way to verify this certificate. It is\n"
" possible that a hostile attacker has replaced the\n"
" server certificate. Continue at your own risk!\n");
printf ("\nAccept this certificate anyway? [no]: ");
fflush (stdout);
if (fgets (buf, sizeof (buf), stdin) && (buf[0] == 'y' || buf[0] == 'Y'))
{
ret = 0;
puts ("\n*** Fine, but don't say I didn't warn you!\n");
}
return ret;
}
static int
init_ssl (config_t * conf)
{
if (!conf->cert_file)
{
puts ("Error, CertificateFile not defined");
return -1;
}
SSL_library_init ();
SSL_load_error_strings ();
SSLContext = SSL_CTX_new (SSLv23_client_method ());
if (access (conf->cert_file, F_OK))
{
if (errno != ENOENT)
{
perror ("access");
return -1;
}
puts
("*** Warning, CertificateFile doesn't exist, can't verify server certificates");
}
else
if (!SSL_CTX_load_verify_locations
(SSLContext, conf->cert_file, NULL))
{
printf ("Error, SSL_CTX_load_verify_locations: %s\n",
ERR_error_string (ERR_get_error (), 0));
return -1;
}
if (!conf->use_sslv2)
SSL_CTX_set_options (SSLContext, SSL_OP_NO_SSLv2);
if (!conf->use_sslv3)
SSL_CTX_set_options (SSLContext, SSL_OP_NO_SSLv3);
if (!conf->use_tlsv1)
SSL_CTX_set_options (SSLContext, SSL_OP_NO_TLSv1);
/* we check the result of the verification after SSL_connect() */
SSL_CTX_set_verify (SSLContext, SSL_VERIFY_NONE, 0);
return 0;
}
#endif /* HAVE_LIBSSL */
static int
socket_read (Socket_t * sock, char *buf, size_t len)
{
#if HAVE_LIBSSL
if (sock->use_ssl)
return SSL_read (sock->ssl, buf, len);
#endif
return read (sock->fd, buf, len);
}
static int
socket_write (Socket_t * sock, char *buf, size_t len)
{
#if HAVE_LIBSSL
if (sock->use_ssl)
return SSL_write (sock->ssl, buf, len);
#endif
return write (sock->fd, buf, len);
}
/* simple line buffering */
static int
buffer_gets (buffer_t * b, char **s)
{
int n;
int start = b->offset;
*s = b->buf + start;
for (;;)
{
/* make sure we have enough data to read the \r\n sequence */
if (b->offset + 1 >= b->bytes)
{
if (start != 0)
{
/* shift down used bytes */
*s = b->buf;
assert (start <= b->bytes);
n = b->bytes - start;
if (n)
memmove (b->buf, b->buf + start, n);
b->offset -= start;
b->bytes = n;
start = 0;
}
n =
socket_read (b->sock, b->buf + b->bytes,
sizeof (b->buf) - b->bytes);
if (n <= 0)
{
if (n == -1)
perror ("read");
else
puts ("EOF");
return -1;
}
b->bytes += n;
}
if (b->buf[b->offset] == '\r')
{
assert (b->offset + 1 < b->bytes);
if (b->buf[b->offset + 1] == '\n')
{
b->buf[b->offset] = 0; /* terminate the string */
b->offset += 2; /* next line */
// assert (strchr (*s, '\r') == 0);
return 0;
}
}
b->offset++;
}
/* not reached */
}
static int
parse_fetch (imap_t * imap, list_t * list)
{
list_t *tmp;
unsigned int uid = 0;
unsigned int mask = 0;
unsigned int size = 0;
message_t *cur;
if (!is_list (list))
return -1;
for (tmp = list->child; tmp; tmp = tmp->next)
{
if (is_atom (tmp))
{
if (!strcmp ("UID", tmp->val))
{
tmp = tmp->next;
if (is_atom (tmp))
{
uid = atoi (tmp->val);
if (uid < imap->minuid)
{
/* already saw this message */
return 0;
}
else if (uid > imap->maxuid)
imap->maxuid = uid;
}
else
puts ("Error, unable to parse UID");
}
else if (!strcmp ("FLAGS", tmp->val))
{
tmp = tmp->next;
if (is_list (tmp))
{
list_t *flags = tmp->child;
for (; flags; flags = flags->next)
{
if (is_atom (flags))
{
if (!strcmp ("\\Seen", flags->val))
mask |= D_SEEN;
else if (!strcmp ("\\Flagged", flags->val))
mask |= D_FLAGGED;
else if (!strcmp ("\\Deleted", flags->val))
mask |= D_DELETED;
else if (!strcmp ("\\Answered", flags->val))
mask |= D_ANSWERED;
else if (!strcmp ("\\Draft", flags->val))
mask |= D_DRAFT;
else if (!strcmp ("\\Recent", flags->val))
mask |= D_RECENT;
else
printf ("Warning, unknown flag %s\n",
flags->val);
}
else
puts ("Error, unable to parse FLAGS list");
}
}
else
puts ("Error, unable to parse FLAGS");
}
else if (!strcmp ("RFC822.SIZE", tmp->val))
{
tmp = tmp->next;
if (is_atom (tmp))
size = atol (tmp->val);
}
}
}
cur = calloc (1, sizeof (message_t));
cur->next = imap->msgs;
imap->msgs = cur;
if (mask & D_DELETED)
imap->deleted++;
cur->uid = uid;
cur->flags = mask;
cur->size = size;
return 0;
}
static void
parse_response_code (imap_t * imap, char *s)
{
char *arg;
if (*s != '[')
return; /* no response code */
s++;
arg = next_arg (&s);
if (!strcmp ("UIDVALIDITY", arg))
{
arg = next_arg (&s);
imap->uidvalidity = atol (arg);
}
else if (!strcmp ("ALERT", arg))
{
/* RFC2060 says that these messages MUST be displayed
* to the user
*/
fputs ("***ALERT*** ", stdout);
puts (s);
}
}
static int
imap_exec (imap_t * imap, const char *fmt, ...)
{
va_list ap;
char tmp[256];
char buf[256];
char *cmd;
char *arg;
char *arg1;
va_start (ap, fmt);
vsnprintf (tmp, sizeof (tmp), fmt, ap);
va_end (ap);
snprintf (buf, sizeof (buf), "%d %s\r\n", ++Tag, tmp);
if (Verbose)
fputs (buf, stdout);
socket_write (imap->sock, buf, strlen (buf));
for (;;)
{
if (buffer_gets (imap->buf, &cmd))
return -1;
if (Verbose)
puts (cmd);
arg = next_arg (&cmd);
if (*arg == '*')
{
arg = next_arg (&cmd);
if (!arg)
{
puts ("Error, unable to parse untagged command");
return -1;
}
if (!strcmp ("NAMESPACE", arg))
{
imap->ns_personal = parse_list (cmd, &cmd);
imap->ns_other = parse_list (cmd, &cmd);
imap->ns_shared = parse_list (cmd, 0);
}
else if (!strcmp ("OK", arg) || !strcmp ("BAD", arg) ||
!strcmp ("NO", arg) || !strcmp ("PREAUTH", arg) ||
!strcmp ("BYE", arg))
{
parse_response_code (imap, cmd);
}
else if (!strcmp ("CAPABILITY", arg))
{
#if HAVE_LIBSSL
while ((arg = next_arg (&cmd)))
{
if (!strcmp ("STARTTLS", arg))
imap->have_starttls = 1;
else if (!strcmp ("AUTH=CRAM-MD5", arg))
imap->have_cram = 1;
else if (!strcmp ("NAMESPACE", arg))
imap->have_namespace = 1;
}
#endif
}
else if ((arg1 = next_arg (&cmd)))
{
if (!strcmp ("EXISTS", arg1))
imap->count = atoi (arg);
else if (!strcmp ("RECENT", arg1))
imap->recent = atoi (arg);
else if (!strcmp ("FETCH", arg1))
{
list_t *list;
list = parse_list (cmd, 0);
if (parse_fetch (imap, list))
{
free_list (list);
return -1;
}
free_list (list);
}
}
else
{
puts ("Error, unable to parse untagged command");
return -1;
}
}
#if HAVE_LIBSSL
else if (*arg == '+')
{
char *resp;
if (!imap->cram)
{
puts ("Error, not doing CRAM-MD5 authentication");
return -1;
}
resp = cram (cmd, imap->box->user, imap->box->pass);
socket_write (imap->sock, resp, strlen (resp));
if (Verbose)
puts (resp);
socket_write (imap->sock, "\r\n", 2);
free (resp);
imap->cram = 0;
}
#endif
else if ((size_t) atol (arg) != Tag)
{
puts ("wrong tag");
return -1;
}
else
{
arg = next_arg (&cmd);
parse_response_code (imap, cmd);
if (!strcmp ("OK", arg))
return 0;
return -1;
}
}
/* not reached */
}
/* `box' is the config info for the maildrop to sync. `minuid' is the
* minimum UID to consider. in normal mode this will be 1, but in --fast
* mode we only fetch messages newer than the last one seen in the local
* mailbox.
*/
imap_t *
imap_open (config_t * box, unsigned int minuid, imap_t * imap)
{
int ret;
int s;
struct sockaddr_in sin;
struct hostent *he;
char *ns_prefix = "";
int reuse = 0;
#if HAVE_LIBSSL
int use_ssl = 0;
#endif
if (imap)
{
/* determine whether or not we can reuse the existing session */
if (strcmp (box->host, imap->box->host) ||
strcmp (box->user, imap->box->user) ||
box->port != imap->box->port
#if HAVE_LIBSSL
/* ensure that security requirements are met */
|| (box->require_ssl ^ imap->box->require_ssl)
|| (box->require_cram ^ imap->box->require_cram)
#endif
)
{
/* can't reuse */
imap_close (imap);
imap = 0;
}
else
{
reuse = 1;
/* reset mailbox-specific state info */
imap->recent = 0;
imap->deleted = 0;
imap->count = 0;
imap->maxuid = 0;
free_message (imap->msgs);
imap->msgs = 0;
}
}
if (!imap)
{
imap = calloc (1, sizeof (imap_t));
imap->sock = calloc (1, sizeof (Socket_t));
imap->buf = calloc (1, sizeof (buffer_t));
imap->buf->sock = imap->sock;
}
imap->box = box;
imap->minuid = minuid;
if (!reuse)
{
/* open connection to IMAP server */
memset (&sin, 0, sizeof (sin));
sin.sin_port = htons (box->port);
sin.sin_family = AF_INET;
printf ("Resolving %s... ", box->host);
fflush (stdout);
he = gethostbyname (box->host);
if (!he)
{
perror ("gethostbyname");
return 0;
}
puts ("ok");
sin.sin_addr.s_addr = *((int *) he->h_addr_list[0]);
s = socket (PF_INET, SOCK_STREAM, 0);
printf ("Connecting to %s:%hu... ", inet_ntoa (sin.sin_addr),
ntohs (sin.sin_port));
fflush (stdout);
if (connect (s, (struct sockaddr *) &sin, sizeof (sin)))
{
perror ("connect");
exit (1);
}
puts ("ok");
imap->sock->fd = s;
}
do
{
/* if we are reusing the existing connection, we can skip the
* authentication steps.
*/
if (!reuse)
{
#if HAVE_LIBSSL
if (!box->use_imaps)
{
/* let's see what this puppy can do... */
if ((ret = imap_exec (imap, "CAPABILITY")))
break;
/* always try to select SSL support if available */
if (imap->have_starttls)
{
if ((ret = imap_exec (imap, "STARTTLS")))
break;
use_ssl = 1;
}
if (!use_ssl)
{
if (box->require_ssl)
{
puts ("Error, SSL support not available");
ret = -1;
break;
}
else
puts ("Warning, SSL support not available");
}
}
else
use_ssl = 1;
if (use_ssl)
{
/* initialize SSL */
if (init_ssl (box))
return 0;
imap->sock->ssl = SSL_new (SSLContext);
SSL_set_fd (imap->sock->ssl, imap->sock->fd);
ret = SSL_connect (imap->sock->ssl);
if (ret <= 0)
{
ret = SSL_get_error (imap->sock->ssl, ret);
printf ("Error, SSL_connect: %s\n",
ERR_error_string (ret, 0));
ret = -1;
break;
}
/* verify the server certificate */
if ((ret = verify_cert (imap->sock->ssl)))
break;
imap->sock->use_ssl = 1;
puts ("SSL support enabled");
if (box->use_imaps)
if ((ret = imap_exec (imap, "CAPABILITY")))
break;
}
#else
if ((ret = imap_exec (imap, "CAPABILITY")))
break;
#endif
puts ("Logging in...");
#if HAVE_LIBSSL
if (imap->have_cram)
{
puts ("Authenticating with CRAM-MD5");
imap->cram = 1;
if ((ret = imap_exec (imap, "AUTHENTICATE CRAM-MD5")))
break;
}
else if (imap->box->require_cram)
{
puts
("Error, CRAM-MD5 authentication is not supported by server");
ret = -1;
break;
}
else
#endif
{
#if HAVE_LIBSSL
if (!use_ssl)
#endif
puts
("*** Warning *** Password is being sent in the clear");
if (
(ret =
imap_exec (imap, "LOGIN \"%s\" \"%s\"", box->user,
box->pass)))
break;
}
/* get NAMESPACE info */
if (box->use_namespace && imap->have_namespace)
{
if ((ret = imap_exec (imap, "NAMESPACE")))
break;
}
} /* !reuse */
/* XXX for now assume personal namespace */
if (imap->box->use_namespace && is_list (imap->ns_personal) &&
is_list (imap->ns_personal->child) &&
is_atom (imap->ns_personal->child->child))
{
ns_prefix = imap->ns_personal->child->child->val;
}
fputs ("Selecting mailbox... ", stdout);
fflush (stdout);
if ((ret = imap_exec (imap, "SELECT %s%s", ns_prefix, box->box)))
break;
printf ("%d messages, %d recent\n", imap->count, imap->recent);
puts ("Reading IMAP mailbox index");
if (imap->count > 0)
{
if ((ret = imap_exec (imap, "UID FETCH %d:* (FLAGS RFC822.SIZE)",
imap->minuid)))
break;
}
}
while (0);
if (ret)
{
imap_exec (imap, "LOGOUT");
close (s);
free (imap->buf);
free (imap);
imap = 0;
}
return imap;
}
void
imap_close (imap_t * imap)
{
puts ("Closing IMAP connection");
imap_exec (imap, "LOGOUT");
close (imap->sock->fd);
free (imap->sock);
free (imap->buf);
free_message (imap->msgs);
memset (imap, 0xff, sizeof (imap_t));
free (imap);
}
/* write a buffer stripping all \r bytes */
static int
write_strip (int fd, char *buf, size_t len)
{
size_t start = 0;
size_t end = 0;
while (start < len)
{
while (end < len && buf[end] != '\r')
end++;
write (fd, buf + start, end - start);
end++;
start = end;
}
return 0;
}
static void
send_server (Socket_t * sock, const char *fmt, ...)
{
char buf[128];
char cmd[128];
va_list ap;
va_start (ap, fmt);
vsnprintf (buf, sizeof (buf), fmt, ap);
va_end (ap);
snprintf (cmd, sizeof (cmd), "%d %s\r\n", ++Tag, buf);
socket_write (sock, cmd, strlen (cmd));
if (Verbose)
fputs (cmd, stdout);
}
int
imap_fetch_message (imap_t * imap, unsigned int uid, int fd)
{
char *cmd;
char *arg;
size_t bytes;
size_t n;
char buf[1024];
send_server (imap->sock, "UID FETCH %d BODY.PEEK[]", uid);
for (;;)
{
if (buffer_gets (imap->buf, &cmd))
return -1;
if (Verbose)
puts (cmd);
if (*cmd == '*')
{
/* need to figure out how long the message is
* * <msgno> FETCH (RFC822 {<size>}
*/
next_arg (&cmd); /* * */
next_arg (&cmd); /* <msgno> */
next_arg (&cmd); /* FETCH */
while ((arg = next_arg (&cmd)) && *arg != '{')
;
if (!arg)
{
puts ("parse error getting size");
return -1;
}
bytes = strtol (arg + 1, 0, 10);
// printf ("receiving %d byte message\n", bytes);
/* dump whats left over in the input buffer */
n = imap->buf->bytes - imap->buf->offset;
if (n > bytes)
{
/* the entire message fit in the buffer */
n = bytes;
}
/* ick. we have to strip out the \r\n line endings, so
* i can't just dump the raw bytes to disk.
*/
write_strip (fd, imap->buf->buf + imap->buf->offset, n);
bytes -= n;
// printf ("wrote %d buffered bytes\n", n);
/* mark that we used part of the buffer */
imap->buf->offset += n;
/* now read the rest of the message */
while (bytes > 0)
{
n = bytes;
if (n > sizeof (buf))
n = sizeof (buf);
n = socket_read (imap->sock, buf, n);
if (n > 0)
{
// printf("imap_fetch_message:%d:read %d bytes\n", __LINE__, n);
write_strip (fd, buf, n);
bytes -= n;
}
else
{
if (n == (size_t) - 1)
perror ("read");
else
puts ("EOF");
return -1;
}
}
// puts ("finished fetching msg");
buffer_gets (imap->buf, &cmd);
if (Verbose)
puts (cmd); /* last part of line */
}
else
{
arg = next_arg (&cmd);
if (!arg || (size_t) atoi (arg) != Tag)
{
puts ("wrong tag");
return -1;
}
arg = next_arg (&cmd);
if (!strcmp ("OK", arg))
return 0;
return -1;
}
}
/* not reached */
}
/* add flags to existing flags */
int
imap_set_flags (imap_t * imap, unsigned int uid, unsigned int flags)
{
char buf[256];
int i;
buf[0] = 0;
for (i = 0; i < D_MAX; i++)
{
if (flags & (1 << i))
snprintf (buf + strlen (buf),
sizeof (buf) - strlen (buf), "%s%s",
(buf[0] != 0) ? " " : "", Flags[i]);
}
return imap_exec (imap, "UID STORE %d +FLAGS.SILENT (%s)", uid, buf);
}
int
imap_expunge (imap_t * imap)
{
return imap_exec (imap, "EXPUNGE");
}