commit f47d0d7c110b0ec61bd2e6ed1d0cdee753d1edd5 Author: Michael Elkins Date: Wed Dec 20 21:41:21 2000 +0000 initial import 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; +}