From f47d0d7c110b0ec61bd2e6ed1d0cdee753d1edd5 Mon Sep 17 00:00:00 2001 From: Michael Elkins Date: Wed, 20 Dec 2000 21:41:21 +0000 Subject: [PATCH] initial import --- AUTHORS | 1 + ChangeLog | 0 Makefile.am | 4 + NEWS | 3 + README | 28 +++ TODO | 1 + configure.in | 9 + imap.c | 531 +++++++++++++++++++++++++++++++++++++++++++++++++ isync.1 | 184 +++++++++++++++++ isync.h | 111 +++++++++++ isyncrc.sample | 28 +++ maildir.c | 208 +++++++++++++++++++ main.c | 398 ++++++++++++++++++++++++++++++++++++ sync.c | 134 +++++++++++++ 14 files changed, 1640 insertions(+) create mode 100644 AUTHORS create mode 100644 ChangeLog create mode 100644 Makefile.am create mode 100644 NEWS create mode 100644 README create mode 100644 TODO create mode 100644 configure.in create mode 100644 imap.c create mode 100644 isync.1 create mode 100644 isync.h create mode 100644 isyncrc.sample create mode 100644 maildir.c create mode 100644 main.c create mode 100644 sync.c diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..8d975c4 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +Michael R. Elkins diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..e69de29 diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..4f13d84 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,4 @@ +bin_PROGRAMS=isync +isync_SOURCES=main.c imap.c sync.c maildir.c +man_MANS=isync.1 +EXTRA_DIST=sample.isyncrc $(man_MANS) diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..cd1c07c --- /dev/null +++ b/NEWS @@ -0,0 +1,3 @@ +[0.1] + +Initial release. diff --git a/README b/README new file mode 100644 index 0000000..ba2ab6b --- /dev/null +++ b/README @@ -0,0 +1,28 @@ + _ +(_)___ _ _ _ __ ___ +| / __| | | | '_ \ / __| +| \__ \ |_| | | | | (__ +|_|___/\__, |_| |_|\___| + |___/ +isync - IMAP4 to maildir mailbox synchronization program +http://www.sigpipe.org/isync/ + +Author: Michael Elkins + +``isync'' is a command line application which synchronizes a local +maildir-style mailbox with a remote IMAP4 mailbox, suitable for use in +IMAP-disconnected mode. Multiple copies of the remote IMAP4 mailbox can be +maintained, and all flags are synchronized. + +``isync'' has been tested with the following IMAP servers: + + Microsoft Exchange 2000 IMAP4rev1 server version 6.0.4417.0 + +* INSTALLING + + ./configure + make install + +* HELP + + Please see the man page for complete documentation. diff --git a/TODO b/TODO new file mode 100644 index 0000000..295df15 --- /dev/null +++ b/TODO @@ -0,0 +1 @@ +add upload support to mirror local msgs on the server diff --git a/configure.in b/configure.in new file mode 100644 index 0000000..fbf0810 --- /dev/null +++ b/configure.in @@ -0,0 +1,9 @@ +AC_INIT(isync.h) +AM_INIT_AUTOMAKE(isync,0.1) +AM_PROG_CC_STDC +if test $CC = gcc; then + CFLAGS="$CFLAGS -pipe" +fi +AC_CHECK_FUNCS(getopt_long) +CFLAGS="$CFLAGS -W -Wall -pedantic -Wmissing-prototypes -Wmissing-declarations" +AC_OUTPUT(Makefile) diff --git a/imap.c b/imap.c new file mode 100644 index 0000000..6cb585e --- /dev/null +++ b/imap.c @@ -0,0 +1,531 @@ +/* isync - IMAP4 to maildir mailbox synchronizer + * Copyright (C) 2000 Michael R. Elkins + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "isync.h" + +const char *Flags[] = { + "\\Seen", + "\\Answered", + "\\Deleted", + "\\Flagged", + "\\Recent", + "\\Draft" +}; + +/* simple line buffering */ +static int +buffer_gets (buffer_t * b, char **s) +{ + int n; + int start = b->offset; + + *s = b->buf + start; + + for (;;) + { + if (b->offset + 2 > b->bytes) + { + /* 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 = n; + start = 0; + + n = read (b->fd, b->buf + b->offset, sizeof (b->buf) - b->offset); + if (n <= 0) + { + if (n == -1) + perror ("read"); + else + puts ("EOF"); + return -1; + } + b->bytes = b->offset + n; + +// printf ("buffer_gets:read %d bytes\n", n); + } + + if (b->buf[b->offset] == '\r') + { + if (b->buf[b->offset + 1] == '\n') + { + b->buf[b->offset] = 0; /* terminate the string */ + b->offset += 2; /* next line */ + return 0; + } + } + + b->offset++; + } + /* not reached */ +} + +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; + message_t **cur = 0; + message_t **rec = 0; + + 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); + write (imap->fd, 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); + arg1 = next_arg (&cmd); + + if (arg1 && !strcmp ("EXISTS", arg1)) + imap->count = atoi (arg); + else if (arg1 && !strcmp ("RECENT", arg1)) + imap->recent = atoi (arg); + else if (!strcmp ("SEARCH", arg)) + { + if (!rec) + { + rec = &imap->msgs; + while (*rec) + rec = &(*rec)->next; + } + while ((arg = next_arg (&cmd))) + { + *rec = calloc (1, sizeof (message_t)); + (*rec)->uid = atoi (arg); + rec = &(*rec)->next; + } + } + else if (arg1 && !strcmp ("FETCH", arg1)) + { + if (!cur) + { + cur = &imap->msgs; + while (*cur) + cur = &(*cur)->next; + } + + /* new message + * * FETCH (UID FLAGS (...)) + */ + arg = next_arg (&cmd); /* (UID */ + arg = next_arg (&cmd); /* */ + *cur = calloc (1, sizeof (message_t)); + (*cur)->uid = atoi (arg); + + arg = next_arg (&cmd); /* FLAGS */ + if (!arg || strcmp ("FLAGS", arg)) + { + printf ("FETCH parse error: expected FLAGS at %s\n", arg); + return -1; + } + + /* if we need to parse additional info, we should keep + * a copy of this `arg' pointer + */ + + cmd++; + arg = strchr (cmd, ')'); + if (!arg) + { + puts ("FETCH parse error"); + return -1; + } + *arg = 0; + + /* parse message flags */ + while ((arg = next_arg (&cmd))) + { + if (!strcmp ("\\Seen", arg)) + (*cur)->flags |= D_SEEN; + else if (!strcmp ("\\Flagged", arg)) + (*cur)->flags |= D_FLAGGED; + else if (!strcmp ("\\Deleted", arg)) + (*cur)->flags |= D_DELETED; + else if (!strcmp ("\\Answered", arg)) + (*cur)->flags |= D_ANSWERED; + else if (!strcmp ("\\Draft", arg)) + (*cur)->flags |= D_DRAFT; + else if (!strcmp ("\\Recent", arg)) + (*cur)->flags |= D_RECENT; + else + printf ("warning, unknown flag %s\n", arg); + } + + cur = &(*cur)->next; + } + } + else if ((size_t) atol (arg) != Tag) + { + puts ("wrong tag"); + return -1; + } + else + { + arg = next_arg (&cmd); + if (!strcmp ("OK", arg)) + return 0; + puts ("IMAP command failed"); + return -1; + } + } + /* not reached */ +} + +static int +fetch_recent_flags (imap_t * imap) +{ + char buf[1024]; + message_t **cur = &imap->recent_msgs; + message_t *tmp; + unsigned int start = -1; + unsigned int last = -1; + int ret = 0; + + buf[0] = 0; + while (*cur) + { + tmp = *cur; + + if (last == (unsigned int) -1) + { + /* init */ + start = tmp->uid; + last = tmp->uid; + } + else if (tmp->uid == last + 1) + last++; + else + { + /* out of sequence */ + if (start == last) + ret = imap_exec (imap, "UID FETCH %d (UID FLAGS)", start); + else + ret = + imap_exec (imap, "UID FETCH %d:%d (UID FLAGS)", start, + last); + start = tmp->uid; + last = tmp->uid; + } + free (tmp); + *cur = (*cur)->next; + } + + if (start != (unsigned int) -1) + { + if (start == last) + ret = imap_exec (imap, "UID FETCH %d (UID FLAGS)", start); + else + ret = + imap_exec (imap, "UID FETCH %d:%d (UID FLAGS)", start, last); + } + + return ret; +} + +imap_t * +imap_open (config_t * box, int fast) +{ + int ret; + imap_t *imap; + int s; + struct sockaddr_in sin; + struct hostent *he; + + /* 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 = calloc (1, sizeof (imap_t)); + imap->fd = s; + //imap->state = imap_state_init; + imap->buf = calloc (1, sizeof (buffer_t)); + imap->buf->fd = s; + imap->box = box; + + puts ("Logging in..."); + ret = imap_exec (imap, "LOGIN %s %s", box->user, box->pass); + if (!ret) + { + fputs ("Selecting mailbox... ", stdout); + fflush (stdout); + ret = imap_exec (imap, "SELECT %s", box->box); + if (!ret) + printf ("%d messages, %d recent\n", imap->count, imap->recent); + } + + if (!ret) + { + if (fast) + { + if (imap->recent > 0) + { + puts ("Fetching info for recent messages"); + ret = imap_exec (imap, "UID SEARCH RECENT"); + if (!ret) + ret = fetch_recent_flags (imap); + } + } + else if (imap->count > 0) + { + puts ("Reading IMAP mailbox index"); + ret = imap_exec (imap, "FETCH 1:%d (UID FLAGS)", imap->count); + } + } + + 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"); +} + +/* 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 (int fd, 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); + write (fd, 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->fd, "UID FETCH %d RFC822.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 + * * FETCH (RFC822 {} + */ + + next_arg (&cmd); /* * */ + next_arg (&cmd); /* */ + next_arg (&cmd); /* FETCH */ + next_arg (&cmd); /* (RFC822 */ + arg = next_arg (&cmd); + 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 = read (imap->fd, 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; + } + break; + } + } + + return 0; +} + +/* 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"); +} diff --git a/isync.1 b/isync.1 new file mode 100644 index 0000000..7f2beed --- /dev/null +++ b/isync.1 @@ -0,0 +1,184 @@ +.ig +\" isync - IMAP4 to maildir mailbox synchronizer +\" Copyright (C) 2000 Michael R. Elkins +\" +\" 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 +.. +.TH isync 1 "2000 Dec 20" +.. +.SH NAME +isync - synchronize IMAP4 and maildir mailboxes +.. +.SH SYNOPSIS +.B isync +[ +.I options... +] +.I file +.. +.SH DESCRIPTION +.B isync +is a command line application which synchronizes a local maildir-style +mailbox with a remote IMAP4 mailbox, suitable for use in IMAP-disconnected +mode. Multiple copies of the remote IMAP4 mailbox can be maintained, and +all flags are synchronized. +.. +.SH OPTIONS +.TP +\fB-c\fR, \fB--config\fR \fIfile\fR +Read configuration from +.I file +By default, configuration is read from ~/.isyncrc if it exists. +.TP +.B -d, --delete +Causes +.B isync +to delete messages from the local maildir mailbox which do not exist on the +IMAP server. By default, +.I dead +messages are +.B not +deleted. +.TP +.B -e, --expunge +Causes +.B isync +to permanently remove all messages marked for deletion in both the local +maildir mailbox and the remote IMAP mailbox. By default, messages are +.B not +expunged. +.TP +.B -f, --fast +Causes +.B isync +to skip the step of synchronzing message flags between the local maildir +mailbox and the IMAP mailbox. Only new messages existing on the server will +be fetched into the local mailbox. +.B NOTE: +This command works by checking the \\Recent flag on messages in the IMAP +mailbox. If you access the IMAP mailbox from multiple locations, the +\\Recent flag will be lost between sessions, so you must do a full +synchronization to fetch the messages which do not exist in the local +mailbox. +.TP +.B -h, --help +Displays a summary of command line options +.TP +\fB-p\fR, \fB--port\fR \fIport\fR +Specifies the port on the IMAP server to connect to (default: 143) +.TP +\fB-r\fR, \fB--remote\fR \fIbox\fR +Specifies the name of the remote IMAP mailbox to synchronize with +(Default: INBOX) +.TP +\fB-s\fR, \fB--host\fR \fIhost\fR +.P +Specifies the hostname of the IMAP server +.TP +\fB-u\fR, \fB--user\fR \fIuser\fR +Specifies the login name to access the IMAP server (default: $USER) +.TP +.B -v, --version +Displays +.B isync +version information +.TP +.B -V, --verbose +Enables +.I verbose +mode, which disables the IMAP network traffic. +.. +.SH CONFIGURATION +.B isync +reads +.I ~/.isyncrc +to load default configuration data. Each line of the configuration file +consists of a command. The following commands are understood: +.TP +\fBMailbox\fR \fIpath\fR +Defines a local maildir mailbox. All configuration commands following this +line, up until the next +.I Mailbox +command, apply to this mailbox only. +.. +.TP +\fBHost\fR \fIname\fR +Defines the DNS name or IP address of the IMAP server +.. +.TP +\fBPort\fR \fIport\fR +Defines the TCP port number on the IMAP server to use (Default: 143) +.. +.TP +\fBBox\fR \fImailbox\fR +Defines the name of the remote IMAP mailbox associated with the local +maildir mailbox (Default: INBOX) +.. +.TP +\fBUser\fR \fIusername\fR +Defines the login name on the IMAP server (Default: current user) +.. +.TP +\fBPass\fR \fIpassword\fR +Defines the password for +.I username +on the IMAP server. Note that this option is +.B NOT +required. If no password is specified in the configuration file, +.B isync +will prompt you for it. +.. +.TP +\fBAlias\fR \fIstring\fR +Defines an alias for the mailbox which can be used as a shortcut on the +command line. +.P +Configuration commands that appear prior to the first +.B Mailbox +command are considered to be +.I global +options which are used as defaults when those specific options are not +specifically set for a defined Mailbox. For example, if you use the same +login name for several IMAP servers, you can put a +.B User +command before the first +.B Mailbox +command, and then leave out the +.B User +command in the sections for each mailbox. +.B isync +will then use the global value by default. +.. +.SH FILES +.TP +.B ~/.isyncrc +Default configuration file +.. +.SH SEE ALSO +mutt(1), maildir(5) +.P +Up to date information on +.B isync +can be found at +http://www.sigpipe.org/isync/. +.. +.SH AUTHOR +Written by Michael R. Elkins . +.. +.SH BUGS +SSL is currently not used when connecting to the IMAP server. A future +version of +.B isync +is expected to support this. diff --git a/isync.h b/isync.h new file mode 100644 index 0000000..a2604e0 --- /dev/null +++ b/isync.h @@ -0,0 +1,111 @@ +/* isync - IMAP4 to maildir mailbox synchronizer + * Copyright (C) 2000 Michael R. Elkins + * + * 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 + +typedef struct +{ + int fd; + char buf[1024]; + int bytes; + int offset; +} +buffer_t; + +typedef struct config config_t; +typedef struct mailbox mailbox_t; +typedef struct message message_t; + +struct config +{ + char *path; + char *host; + int port; + char *user; + char *pass; + char *box; + char *alias; + config_t *next; +}; + +/* struct representing local mailbox file */ +struct mailbox +{ + char *path; + message_t *msgs; + unsigned int changed:1; +}; + +/* message dispositions */ +#define D_SEEN (1<<0) +#define D_ANSWERED (1<<1) +#define D_DELETED (1<<2) +#define D_FLAGGED (1<<3) +#define D_RECENT (1<<4) +#define D_DRAFT (1<<5) +#define D_MAX 6 + +struct message +{ + char *file; + unsigned int uid; + unsigned int flags; + message_t *next; + unsigned int processed:1; /* message has already been evaluated */ + unsigned int new:1; /* message is in the new/ subdir */ + unsigned int changed:1; /* flags changed */ + unsigned int dead:1; /* message doesn't exist on the server */ +}; + +/* imap connection info */ +typedef struct +{ + int fd; /* server socket */ + unsigned int count; /* # of msgs */ + unsigned int recent; /* # of recent messages */ + buffer_t *buf; /* input buffer for reading server output */ + message_t *msgs; /* list of messages on the server */ + config_t *box; /* mailbox to open */ + message_t *recent_msgs; /* list of recent messages - only contains + * UID to be used in a FETCH FLAGS command + */ +} +imap_t; + +/* flags for sync_mailbox */ +#define SYNC_FAST (1<<0) /* don't sync flags, only fetch new msgs */ +#define SYNC_DELETE (1<<1) /* delete local that don't exist on server */ + +extern config_t global; +extern unsigned int Tag; +extern char Hostname[256]; +extern int Verbose; + +char *next_arg (char **); + +int sync_mailbox (mailbox_t *, imap_t *, int); + +void imap_close (imap_t *); +int imap_fetch_message (imap_t *, unsigned int, int); +int imap_set_flags (imap_t *, unsigned int, unsigned int); +int imap_expunge (imap_t *); +imap_t *imap_open (config_t *, int); + +mailbox_t *maildir_open (const char *, int fast); +int maildir_expunge (mailbox_t *, int); +int maildir_sync (mailbox_t *); diff --git a/isyncrc.sample b/isyncrc.sample new file mode 100644 index 0000000..90c4442 --- /dev/null +++ b/isyncrc.sample @@ -0,0 +1,28 @@ +# Global configuration section +# Values here are used as defaults for any following Mailbox section that +# doesn't specify it. + +# my default username, if different from the local username +User me +#Port 143 +#Box INBOX + +### +### work mailbox +### + +Mailbox /home/me/Mail/work +Host work.host.com +Pass xxxxxxxx +# define a shortcut so I can just use "isync work" from the command line +Alias work + +### +### personal mailbox +### + +Mailbox /home/me/Mail/personal +Host host.play.com +# use a non-default port for this connection +Port 6789 +Alias personal diff --git a/maildir.c b/maildir.c new file mode 100644 index 0000000..e4914d8 --- /dev/null +++ b/maildir.c @@ -0,0 +1,208 @@ +/* isync - IMAP4 to maildir mailbox synchronizer + * Copyright (C) 2000 Michael R. Elkins + * + * 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 +#include +#include +#include +#include +#include +#include +#include "isync.h" + +/* 2, */ +static void +parse_info (message_t * m, char *s) +{ + if (*s == '2' && *(s + 1) == ',') + { + s += 2; + while (*s) + { + if (*s == 'F') + m->flags |= D_FLAGGED; + else if (*s == 'R') + m->flags |= D_ANSWERED; + else if (*s == 'T') + m->flags |= D_DELETED; + else if (*s == 'S') + m->flags |= D_SEEN; + s++; + } + } +} + +/* open a maildir mailbox. if `fast' is nonzero, we just check to make + * sure its a valid mailbox and don't actually parse it. any IMAP messages + * with the \Recent flag set are guaranteed not to be in the mailbox yet, + * so we can save a lot of time when the user just wants to fetch new messages + * without syncing the flags. + */ +mailbox_t * +maildir_open (const char *path, int fast) +{ + char buf[_POSIX_PATH_MAX]; + DIR *d; + struct dirent *e; + message_t **cur; + message_t *p; + mailbox_t *m; + char *s; + int count = 0; + + /* check to make sure this looks like a valid maildir box */ + snprintf (buf, sizeof (buf), "%s/new", path); + if (access (buf, F_OK)) + { + perror ("access"); + return 0; + } + snprintf (buf, sizeof (buf), "%s/cur", path); + if (access (buf, F_OK)) + { + perror ("access"); + return 0; + } + m = calloc (1, sizeof (mailbox_t)); + m->path = strdup (path); + + if (fast) + return m; + + cur = &m->msgs; + for (; count < 2; count++) + { + /* read the msgs from the new subdir */ + snprintf (buf, sizeof (buf), "%s/%s", path, + (count == 0) ? "new" : "cur"); + d = opendir (buf); + if (!d) + { + perror ("opendir"); + return 0; + } + while ((e = readdir (d))) + { + if (*e->d_name == '.') + continue; /* skip dot-files */ + *cur = calloc (1, sizeof (message_t)); + p = *cur; + p->file = strdup (e->d_name); + p->uid = -1; + p->flags = (count == 1) ? D_SEEN : 0; + p->new = (count == 0); + + /* filename format is something like: + * .UID:2, + * This is completely non-standard, but in order for mail + * clients to understand the flags, we have to use the + * standard :info as described by the qmail spec + */ + s = strstr (p->file, "UID"); + if (!s) + puts ("warning, no uid for message"); + else + { + p->uid = strtol (s + 3, &s, 10); + if (*s && *s != ':') + { + puts ("warning, unable to parse uid"); + p->uid = -1; /* reset */ + } + } + + s = strchr (p->file, ':'); + if (s) + parse_info (p, s + 1); + cur = &p->next; + } + closedir (d); + } + return m; +} + +/* permanently remove messages from a maildir mailbox. if `dead' is nonzero, + * we only remove the messags marked dead. + */ +int +maildir_expunge (mailbox_t * mbox, int dead) +{ + message_t **cur = &mbox->msgs; + message_t *tmp; + char path[_POSIX_PATH_MAX]; + + while (*cur) + { + if ((dead == 0 && (*cur)->flags & D_DELETED) || + (dead && (*cur)->dead)) + { + tmp = *cur; + *cur = (*cur)->next; + snprintf (path, sizeof (path), "%s/%s/%s", + mbox->path, tmp->new ? "new" : "cur", tmp->file); + if (unlink (path)) + perror ("unlink"); + free (tmp->file); + free (tmp); + } + else + cur = &(*cur)->next; + } + return 0; +} + +int +maildir_sync (mailbox_t * mbox) +{ + message_t *cur = mbox->msgs; + char path[_POSIX_PATH_MAX]; + char oldpath[_POSIX_PATH_MAX]; + char *p; + + if (mbox->changed) + { + for (; cur; cur = cur->next) + { + if (cur->changed) + { + /* generate old path */ + snprintf (oldpath, sizeof (oldpath), "%s/%s/%s", + mbox->path, cur->new ? "new" : "cur", cur->file); + + /* truncate old flags (if present) */ + p = strchr (cur->file, ':'); + if (p) + *p = 0; + + p = strrchr (cur->file, '/'); + + /* generate new path */ + snprintf (path, sizeof (path), "%s/%s%s:2,%s%s%s%s", + mbox->path, (cur->flags & D_SEEN) ? "cur" : "new", + cur->file, (cur->flags & D_FLAGGED) ? "F" : "", + (cur->flags & D_ANSWERED) ? "R" : "", + (cur->flags & D_SEEN) ? "S" : "", + (cur->flags & D_DELETED) ? "T" : ""); + + if (rename (oldpath, path)) + perror ("rename"); + } + } + } + return 0; +} diff --git a/main.c b/main.c new file mode 100644 index 0000000..85eb993 --- /dev/null +++ b/main.c @@ -0,0 +1,398 @@ +/* isync - IMAP4 to maildir mailbox synchronizer + * Copyright (C) 2000 Michael R. Elkins + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include "isync.h" + +#if HAVE_GETOPT_LONG +#define _GNU_SOURCE +#include + +struct option Opts[] = { + {"config", 1, NULL, 'c'}, + {"delete", 0, NULL, 'd'}, + {"expunge", 0, NULL, 'e'}, + {"fast", 0, NULL, 'f'}, + {"help", 0, NULL, 'h'}, + {"remote", 1, NULL, 'r'}, + {"host", 1, NULL, 's'}, + {"port", 1, NULL, 'p'}, + {"user", 1, NULL, 'u'}, + {"version", 0, NULL, 'v'}, + {"verbose", 0, NULL, 'V'}, + {0, 0, 0, 0} +}; +#endif + +config_t global; +unsigned int Tag = 0; +static config_t *box = 0; +char Hostname[256]; +int Verbose = 0; + +static void +version (void) +{ + printf ("%s %s\n", PACKAGE, VERSION); + exit (0); +} + +static void +usage (void) +{ + printf ("%s %s IMAP4 to maildir synchronizer\n", PACKAGE, VERSION); + puts ("Copyright (C) 2000 Michael R. Elkins "); + printf ("usage: %s [ flags ] mailbox\n", PACKAGE); + puts + (" -c, --config CONFIG read an alternate config file (default: ~/.isyncrc)"); + puts + (" -d, --delete delete local msgs that don't exist on the server"); + puts + (" -e, --expunge expunge deleted messages from the server"); + puts (" -f, --fast only fetch new messages"); + puts (" -h, --help display this help message"); + puts (" -p, --port PORT server IMAP port"); + puts (" -r, --remote BOX remote mailbox"); + puts (" -s, --host HOST IMAP server address"); + puts (" -u, --user USER IMAP user name"); + puts (" -v, --version display version"); + puts + (" -V, --verbose verbose mode (display network traffic)"); + exit (0); +} + +static char * +enter_password (void) +{ + struct termios t; + char pass[32]; + + tcgetattr (0, &t); + t.c_lflag &= ~ECHO; + tcsetattr (0, TCSANOW, &t); + printf ("Password: "); + fflush (stdout); + pass[sizeof (pass) - 1] = 0; + fgets (pass, sizeof (pass) - 1, stdin); + if (pass[0]) + pass[strlen (pass) - 1] = 0; /* kill newline */ + t.c_lflag |= ECHO; + tcsetattr (0, TCSANOW, &t); + puts (""); + return strdup (pass); +} + +static void +load_config (char *where) +{ + char path[_POSIX_PATH_MAX]; + char buf[1024]; + struct passwd *pw; + config_t **cur = &box; + char *p; + int line = 0; + FILE *fp; + + if (!where) + { + pw = getpwuid (getuid ()); + snprintf (path, sizeof (path), "%s/.isyncrc", pw->pw_dir); + where = path; + } + printf ("Reading %s\n", where); + + fp = fopen (where, "r"); + if (!fp) + { + if (errno != ENOENT) + { + perror ("fopen"); + return; + } + } + while ((fgets (buf, sizeof (buf) - 1, fp))) + { + if (buf[0]) + buf[strlen (buf) - 1] = 0; + line++; + if (buf[0] == '#') + continue; + p = buf; + while (*p && !isspace (*p)) + p++; + while (isspace (*p)) + p++; + if (!strncmp ("mailbox", buf, 7)) + { + if (*cur) + cur = &(*cur)->next; + *cur = calloc (1, sizeof (config_t)); + (*cur)->path = strdup (p); + } + else if (!strncmp ("host", buf, 4)) + { + if (*cur) + (*cur)->host = strdup (p); + else + global.host = strdup (p); + } + else if (!strncmp ("user", buf, 4)) + { + if (*cur) + (*cur)->user = strdup (p); + else + global.user = strdup (p); + } + else if (!strncmp ("pass", buf, 4)) + { + if (*cur) + (*cur)->pass = strdup (p); + else + global.pass = strdup (p); + } + else if (!strncmp ("port", buf, 4)) + { + if (*cur) + (*cur)->port = atoi (p); + else + global.port = atoi (p); + } + else if (!strncmp ("box", buf, 3)) + { + if (*cur) + (*cur)->box = strdup (p); + else + global.box = strdup (p); + } + else if (!strncmp ("alias", buf, 5)) + { + if (*cur) + (*cur)->alias = strdup (p); + } + else if (buf[0]) + printf ("%s:%d:unknown command:%s", path, line, buf); + } + fclose (fp); +} + +static config_t * +find_box (const char *s) +{ + config_t *p = box; + + for (; p; p = p->next) + if (!strcmp (s, p->path) || (p->alias && !strcmp (s, p->alias))) + return p; + return 0; +} + +char * +next_arg (char **s) +{ + char *ret; + + if (!s) + return 0; + if (!*s) + return 0; + while (isspace (**s)) + (*s)++; + if (!**s) + { + *s = 0; + return 0; + } + ret = *s; + while (**s && !isspace (**s)) + (*s)++; + if (**s) + *(*s)++ = 0; + if (!**s) + *s = 0; + return ret; +} + +int +main (int argc, char **argv) +{ + int i; + config_t *box; + mailbox_t *mail; + imap_t *imap; + int expunge = 0; /* by default, don't delete anything */ + int fast = 0; + int delete = 0; + char *config = 0; + struct passwd *pw; + + pw = getpwuid (getuid ()); + + /* defaults */ + memset (&global, 0, sizeof (global)); + global.port = 143; + global.box = "INBOX"; + global.user = strdup (pw->pw_name); + +#if HAVE_GETOPT_LONG + while ((i = getopt_long (argc, argv, "defhp:u:r:s:vV", Opts, NULL)) != -1) +#else + while ((i = getopt (argc, argv, "defhp:u:r:s:vV")) != -1) +#endif + { + switch (i) + { + case 'c': + config = optarg; + break; + case 'd': + delete = 1; + break; + case 'e': + expunge = 1; + break; + case 'f': + fast = 1; + break; + case 'p': + global.port = atoi (optarg); + break; + case 'r': + global.box = optarg; + break; + case 's': + global.host = optarg; + break; + case 'u': + free (global.user); + global.user = optarg; + break; + case 'V': + Verbose = 1; + break; + case 'v': + version (); + default: + usage (); + } + } + + if (!argv[optind]) + { + puts ("No box specified"); + usage (); + } + + gethostname (Hostname, sizeof (Hostname)); + + load_config (config); + + box = find_box (argv[optind]); + if (!box) + { + /* if enough info is given on the command line, don't worry if + * the mailbox isn't defined. + */ + if (!global.host) + { + puts ("No such mailbox"); + exit (1); + } + global.path = argv[optind]; + box = &global; + } + + /* fill in missing info with defaults */ + if (!box->pass) + { + if (!global.pass) + { + box->pass = enter_password (); + if (!box->pass) + { + puts ("Aborting, no password"); + exit (1); + } + } + else + box->pass = global.pass; + } + if (!box->user) + box->user = global.user; + if (!box->port) + box->port = global.port; + if (!box->host) + box->host = global.host; + if (!box->box) + box->box = global.box; + + printf ("Reading %s\n", box->path); + mail = maildir_open (box->path, fast); + if (!mail) + { + puts ("Unable to load mailbox"); + exit (1); + } + + imap = imap_open (box, fast); + if (!imap) + exit (1); + + puts ("Synchronizing"); + i = 0; + i |= (fast) ? SYNC_FAST : 0; + i |= (delete) ? SYNC_DELETE : 0; + if (sync_mailbox (mail, imap, i)) + exit (1); + + if (!fast) + { + if (expunge) + { + /* remove messages marked for deletion */ + puts ("Expunging messages"); + if (imap_expunge (imap)) + exit (1); + if (maildir_expunge (mail, 0)) + exit (1); + } + /* remove messages deleted from server. this can safely be an + * `else' clause since dead messages are marked as deleted by + * sync_mailbox. + */ + else if (delete) + maildir_expunge (mail, 1); + + /* write changed flags back to the mailbox */ + printf ("Committing changes to %s\n", mail->path); + if (maildir_sync (mail)) + exit (1); + } + + /* gracefully close connection to the IMAP server */ + imap_close (imap); + + exit (0); +} diff --git a/sync.c b/sync.c new file mode 100644 index 0000000..001774a --- /dev/null +++ b/sync.c @@ -0,0 +1,134 @@ +/* isync - IMAP4 to maildir mailbox synchronizer + * Copyright (C) 2000 Michael R. Elkins + * + * 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 +#include +#include +#include +#include +#include +#include +#include "isync.h" + +static unsigned int MaildirCount = 0; + +static message_t * +find_msg (message_t * list, unsigned int uid) +{ + for (; list; list = list->next) + if (list->uid == uid) + return list; + return 0; +} + +int +sync_mailbox (mailbox_t * mbox, imap_t * imap, int flags) +{ + message_t *cur; + message_t *tmp; + char path[_POSIX_PATH_MAX]; + char newpath[_POSIX_PATH_MAX]; + char *p; + int fd; + + for (cur = mbox->msgs; cur; cur = cur->next) + { + tmp = find_msg (imap->msgs, cur->uid); + if (!tmp) + { + printf ("warning, uid %d doesn't exist on server\n", cur->uid); + if (flags & SYNC_DELETE) + { + cur->flags |= D_DELETED; + cur->dead = 1; + } + continue; + } + tmp->processed = 1; + + if (!(flags & SYNC_FAST)) + { + /* check if local flags are different from server flags. + * ignore \Recent and \Draft + */ + if (cur->flags != (tmp->flags & ~(D_RECENT | D_DRAFT))) + { + /* set local flags that don't exist on the server */ + imap_set_flags (imap, cur->uid, cur->flags & ~tmp->flags); + + /* update local flags */ + cur->flags |= (tmp->flags & ~(D_RECENT | D_DRAFT)); + cur->changed = 1; + mbox->changed = 1; + } + } + } + + fputs ("Fetching new messages", stdout); + fflush (stdout); + for (cur = imap->msgs; cur; cur = cur->next) + { + if (!cur->processed) + { + /* new message on server */ + fputs (".", stdout); + fflush (stdout); + + /* create new file */ + snprintf (path, sizeof (path), "%s/tmp/%s.%ld_%d.%d.UID%d", + mbox->path, Hostname, time (0), MaildirCount++, + getpid (), cur->uid); + + if (cur->flags) + { + /* append flags */ + snprintf (path + strlen (path), sizeof (path) - strlen (path), + ":2,%s%s%s%s", + (cur->flags & D_FLAGGED) ? "F" : "", + (cur->flags & D_ANSWERED) ? "R" : "", + (cur->flags & D_SEEN) ? "S" : "", + (cur->flags & D_DELETED) ? "T" : ""); + } + +// printf("creating %s\n", path); + fd = open (path, O_WRONLY | O_CREAT | O_EXCL, 0600); + if (fd < 0) + { + perror ("open"); + continue; + } + + imap_fetch_message (imap, cur->uid, fd); + + close (fd); + + p = strrchr (path, '/'); + + snprintf (newpath, sizeof (newpath), "%s/%s%s", mbox->path, + (cur->flags & D_SEEN) ? "cur" : "new", p); + +// printf ("moving %s to %s\n", path, newpath); + + if (rename (path, newpath)) + perror ("rename"); + } + } + puts (""); + + return 0; +}