Michael Elkins преди 24 години
ревизия
f47d0d7c11
променени са 14 файла, в които са добавени 1640 реда и са изтрити 0 реда
  1. 1 0
      AUTHORS
  2. 0 0
      ChangeLog
  3. 4 0
      Makefile.am
  4. 3 0
      NEWS
  5. 28 0
      README
  6. 1 0
      TODO
  7. 9 0
      configure.in
  8. 531 0
      imap.c
  9. 184 0
      isync.1
  10. 111 0
      isync.h
  11. 28 0
      isyncrc.sample
  12. 208 0
      maildir.c
  13. 398 0
      main.c
  14. 134 0
      sync.c

+ 1 - 0
AUTHORS

@@ -0,0 +1 @@
+Michael R. Elkins <me@mutt.org>

+ 0 - 0
ChangeLog


+ 4 - 0
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)

+ 3 - 0
NEWS

@@ -0,0 +1,3 @@
+[0.1]
+
+Initial release.

+ 28 - 0
README

@@ -0,0 +1,28 @@
+ _                      
+(_)___ _   _ _ __   ___ 
+| / __| | | | '_ \ / __|
+| \__ \ |_| | | | | (__ 
+|_|___/\__, |_| |_|\___|
+       |___/            
+isync - IMAP4 to maildir mailbox synchronization program
+http://www.sigpipe.org/isync/
+
+Author: Michael Elkins <me@mutt.org>
+
+``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.

+ 1 - 0
TODO

@@ -0,0 +1 @@
+add upload support to mirror local msgs on the server

+ 9 - 0
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)

+ 531 - 0
imap.c

@@ -0,0 +1,531 @@
+/* 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>
+#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
+		 *      * <N> FETCH (UID <uid> FLAGS (...))
+		 */
+		arg = next_arg (&cmd);	/* (UID */
+		arg = next_arg (&cmd);	/* <uid> */
+		*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
+	     * * <msgno> FETCH (RFC822 {<size>}
+	     */
+
+	    next_arg (&cmd);	/* * */
+	    next_arg (&cmd);	/* <msgno> */
+	    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");
+}

+ 184 - 0
isync.1

@@ -0,0 +1,184 @@
+.ig
+\" 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
+..
+.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 <me@mutt.org>.
+..
+.SH BUGS
+SSL is currently not used when connecting to the IMAP server.  A future
+version of
+.B isync
+is expected to support this.

+ 111 - 0
isync.h

@@ -0,0 +1,111 @@
+/* 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 <stdarg.h>
+
+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 *);

+ 28 - 0
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

+ 208 - 0
maildir.c

@@ -0,0 +1,208 @@
+/* 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 <limits.h>
+#include <stdlib.h>
+#include <string.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+#include "isync.h"
+
+/* 2,<flags> */
+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:
+	     * <unique-prefix>.UID<n>:2,<flags>
+	     * 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;
+}

+ 398 - 0
main.c

@@ -0,0 +1,398 @@
+/* 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 <stdlib.h>
+#include <unistd.h>
+#include <limits.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <ctype.h>
+#include <termios.h>
+#include "isync.h"
+
+#if HAVE_GETOPT_LONG
+#define _GNU_SOURCE
+#include <getopt.h>
+
+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 <me@mutt.org>");
+    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);
+}

+ 134 - 0
sync.c

@@ -0,0 +1,134 @@
+/* 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 <stdio.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <time.h>
+#include <fcntl.h>
+#include <string.h>
+#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;
+}