isync

mailbox synchronization program
git clone https://git.code.sf.net/p/isync/isync
Log | Files | Refs | README | LICENSE

commit 130664b622b7169adf35ae4c01177f8b31505321
parent 8d1b26aebe451447ed2ec6fbfb370cd7017db00b
Author: Oswald Buddenhagen <ossi@users.sf.net>
Date:   Sat, 27 Mar 2004 16:07:20 +0000

The Big Rewrite. too many change to list them all.

as opposed to earlier threats, BerkDB was not entirely dropped; i
suppose the isync 0.7 -> 0.8 change had a reason, so i added an
alternative UID storage scheme.
note that BDB 4.0 is not sufficient, as the db->open function changed in
an incompatible way ...

i updated the debian packaging except for a changelog entry.
note that i removed the upgrade blurb, as upstream now has a smooth
upgrade path down to at least isync 0.4.

Diffstat:
M.cvsignore | 4++++
MMakefile.am | 7+++++--
MNEWS | 24++++++++++++++++++++++++
MREADME | 77+++++++++++++++++++++++++++++++++++++++++++++++------------------------------
MTODO | 62+++++++++++++++++++++++++++++++++++++-------------------------
Mconfigure.in | 49++++++++++++++++++++++++++++++-------------------
Ddebian/NEWS | 16----------------
Ddebian/README.Debian | 22----------------------
Ddebian/config | 8--------
Mdebian/control | 27++++++++++++++++-----------
Mdebian/patches/00list | 1-
Ddebian/patches/20-cleanup.dpatch | 146-------------------------------------------------------------------------------
Ddebian/patches/30-async-imap.dpatch | 458-------------------------------------------------------------------------------
Ddebian/po/POTFILES.in | 1-
Ddebian/po/fr.po | 74--------------------------------------------------------------------------
Ddebian/po/templates.pot | 60------------------------------------------------------------
Ddebian/preinst | 19-------------------
Mdebian/rules | 3+--
Ddebian/templates | 21---------------------
Disync.1 | 344-------------------------------------------------------------------------------
Misync.spec.in | 17+++++++++++------
Msrc/.cvsignore | 3++-
Msrc/Makefile.am | 21++++++++++++++++-----
Csrc/.cvsignore -> src/compat/.cvsignore | 0
Asrc/compat/Makefile.am | 8++++++++
Asrc/compat/config.c | 443+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/compat/convert.c | 259+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/compat/isync.1 | 318+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/compat/isync.h | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Risyncrc.sample -> src/compat/isyncrc.sample | 0
Asrc/compat/main.c | 431+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/compat/util.c | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/config.c | 608++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Dsrc/cram.c | 89-------------------------------------------------------------------------------
Dsrc/dotlock.c | 102-------------------------------------------------------------------------------
Dsrc/dotlock.h | 25-------------------------
Asrc/drv_imap.c | 1817+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/drv_maildir.c | 1153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/imap.c | 1413-------------------------------------------------------------------------------
Msrc/isync.h | 403+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Dsrc/list.c | 179-------------------------------------------------------------------------------
Dsrc/maildir.c | 459-------------------------------------------------------------------------------
Msrc/main.c | 965++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Asrc/mbsync.1 | 471+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/mbsyncrc.sample | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/mdconvert.1 | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/mdconvert.c | 256+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/sync.c | 1244+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Asrc/util.c | 389+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
49 files changed, 8169 insertions(+), 4716 deletions(-)

diff --git a/.cvsignore b/.cvsignore @@ -7,12 +7,16 @@ build-stamp config.h config.h.in config.cache +config.guess config.log config.status +config.sub configure configure.lineno configure-stamp isync.spec isync-*.tar.gz patch-stamp +stamp-h +stamp-h.in stamp-h1 diff --git a/Makefile.am b/Makefile.am @@ -1,6 +1,6 @@ SUBDIRS = src -man_MANS = isync.1 -EXTRA_DIST = debian isyncrc.sample isync.spec $(man_MANS) +bin_SCRIPTS = get-cert +EXTRA_DIST = debian isync.spec $(bin_SCRIPTS) log: @perl -p -e "s/^(\\S+)\\s+(\\S.+\\S)\\s+(\\S+)\\s*\$$/\$$1:'\$$2 <\$$3>'\\n/" < ../CVSROOT/accounts > .usermap @@ -16,6 +16,9 @@ deb-clean: distdir distclean: deb-clean +dist-hook: + find $(distdir)/debian \( -name CVS -o -name .cvsignore -o -name .#\*# -o -type l \) -print0 | xargs -0r rm -rf + rpm: make dist cp $(PACKAGE)-$(VERSION).tar.gz /usr/src/rpm/SOURCES diff --git a/NEWS b/NEWS @@ -1,3 +1,27 @@ +[1.0.0] + +Essentially a rewrite. Synchronization state storage concept, configuration +and command line changed entirely. +But you needn't to worry about the upgrade, as a fully automated migration +path is provided, even for users of isync 0.7 and below. +Still, you should re-read the manual to be able to take full advantage of the +new features: + +The supported mailbox types can be freely paired. +A possible application of this is using a local IMAP server to access +mailboxes that are not natively supported yet. + +Message deletions (expunges) are now propagated both ways, so there is no need +for using mutt with maildir_trash any more. + +Additional trash options added. + +`OneToOne' replaced by something more flexible. + +Partial support for IMAP pipelining (streaming, parallelization) added. +Makes flag change propagation much faster - this affects every message that +becomes Seen/Read. + [0.9] Added Tunnel directive to allow the user to specify a shell command to run diff --git a/README b/README @@ -4,52 +4,69 @@ | \__ \ |_| | | | | (__ |_|___/\__, |_| |_|\___| |___/ -isync - IMAP4 to maildir mailbox synchronization program +isync/mbsync - free (GPL) mailbox synchronization program http://isync.sf.net/ -Author: Michael Elkins <me@mutt.org> -Current maintainer: Oswald Buddenhagen <ossi@users.sf.net> +See AUTHORS for contact information. -``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. +``mbsync'' is a command line application which synchronizes mailboxes; +currently Maildir and IMAP4 mailboxes are supported. New messages, message +deletions and flag changes can be propagated both ways. +``mbsync'' is suitable for use in IMAP-disconnected mode. -* Features: +Synchronization is based on unique message identifiers (UIDs), so no +identification conflicts can occur (as opposed to some other mail +synchronizers). +Synchronization state is kept in one local text file per mailbox pair; +multiple replicas of a mailbox can be maintained. - * Fast mode for fetching new mail only - * Supports imaps: (port 993) TLS/SSL connections - * Supports STARTTLS (RFC2595) for confidentiality - * Supports NAMESPACE (RFC2342) - * Supports CRAM-MD5 (RFC2095) for authentication +isync is the project name, while mbsync is the current executable name; this +change was necessary because of massive changes in the user interface. An +isync executable still exists; it is a compatibility wrapper around mbsync. + +* Features + + * Fine-grained selection of synchronization operations to perform + * Synchronizes single mailboxes or entire mailbox collections + * Partial mirrors possible: keep only the latest messages locally + * Trash functionality: backup messages before removing them + * IMAP features: + * Supports TLS/SSL via imaps: (port 993) and STARTTLS (RFC2595) + * Supports CRAM-MD5 (RFC2195) for authentication + * Supports NAMESPACE (RFC2342) for simplified configuration + * Pipelining for maximum speed (currently only partially implemented) * Compatibility - ``isync'' has been tested with the following IMAP servers: + isync should work fairly well with any IMAP4 compliant server; + particularily efficient with those that support the UIDPLUS and LITERAL+ + extensions. - * Microsoft Exchange 2000 IMAP4rev1 server version 6.0.4417.0 - * Courier-IMAP 1.2.3 - * WU-IMAP 2000 - * Domino IMAP4 Server Release 5.0.8 + Courier 1.4.3 is known to be buggy, version 1.7.3 works fine. -* Platforms + c-client (UW-IMAP, Pine) is mostly fine, but tends to change UIDVALIDITY + pretty often when used with unix/mbox mailboxes, making isync refuse + synchronization. + The "cure" is to simply copy the new UIDVALIDITY from the affected + mailbox to mbsync's state file. This is a Bad Hack (TM), but it works - + use at your own risk (if the UIDVALIDITY change was genuine, this will + delete all messages in the affected mailbox - not that this ever + happened to me). - ``isync'' has successfully been compiled on: +* Platforms - * Linux - * Solaris 2.7 - * OpenBSD 2.8 - * FreeBSD 4.3 + At some point, ``isync'' has successfully run on: + Linux, Solaris 2.7, OpenBSD 2.8, FreeBSD 4.3, Cygwin * Requirements - OpenSSL for TLS/SSL support (optional) + OpenSSL for TLS/SSL support (optional) -* INSTALLING +* Installation - ./configure - make install + ./configure + make install -* HELP +* Help - Please see the man page for complete documentation. + Please see the man page for complete documentation. diff --git a/TODO b/TODO @@ -1,39 +1,51 @@ -change of UIDVALIDITY shouldn't be considered fatal for the imap connection. -maybe the error handling needs to be cleaned up in general. +make SSL certificate validation more automatic. -don't require maildir_trash. currently MaxMessages gets into the way of -simply removing it; that is fixable with some shuffling, though. +add asynchronous operation to remote mailbox drivers. this is actually +what prevents us from simply using c-client and thus becoming mailsync. -refactor mailbox support. create proper mailbox drivers; handle imap and -maildir (and anything else) symmetrically; decouple UID->message mapping -from sync database - should use the same UID storing schemes as c-client -(pine, uw-imap) does, at least optionally, i think. +handle custom flags (keywords). -add asynchrounous operation to remote mailbox drivers. this is actually -what prevents us from simply using c-client for the previous point and -thus simply becoming mailsync. +fix maildir_{open_store,list} to handle partial names (last char not slash). -store message flags in sync database, so _un_setting them will be properly -synced as well. +add a way to automatically create and sync subfolders. -handle custom imap flags. currently, isync just fails horribly if it -encounters some. +could store TUID even when UIDPLUS is supported. would avoid duplicated +messages after abort before new UID arrives. -add options for fine-grained control of syncing operations (--new, --delete & ---flags) and direction (--push & --pull). +decouple TUID search from append. that's a prerequisite for usable +MULTIAPPEND, and is generally good for async. should be way faster, too, +as it saves repeated mailbox rescans with single-file formats. -add support for syncing with other: and shared: via NAMESPACE +use MULTIAPPEND and FETCH with multiple messages. -isync ignores asynchronous notifications (untagged responses), so mail -arriving during a fetch will not be fetched in the current run any more. +create dummies describing MIME structure of messages bigger than MaxSize. +flagging the dummy would fetch the real message. possibly remove --renew. -add a way to automatically create and sync IMAP subfolders. +don't SELECT boxes unless really needed; in particular not for appending, +and in write-only mode not before changes are made. -make the command line take precedence over the config file. +possibly request message attributes on a per-message basis from the drivers. +considerations: +- record non-existing UID ranges in the sync database, so IMAP FETCHes needn't + to exclude anyway non-existing messages explicitly. +- when detect unborn pairs and orphaned messages being gone? implied by expunge: + with trashing, by local driver, or of messages we deleted in this run. the + remaining cases could be handled by automatic periodical cleanup passes, an + explicit --cleanup action, or be implied by one of the other actions. +- the benefit of this is questionable, as fine-grained requests will result + in sending huge amounts of data, and upstream is often way slower than + downstream. -possibly timestamp mails with remote arrival date. +maildir: possibly timestamp mails with remote arrival date. -possibly recover from UIDVALIDITY change by resyncing according to message -IDs - this is a pretty common condition with uw-imap. +maybe throw out the ctx->recent stuff - it's used only for one info message. possibly use ^[[1m to highlight error messages. + +consider alternative trash implementation: trash only messages we delete, +and trash before marking them deleted in the mailbox. downside: all other +programs have to do the same. and what if the deleted flag is unset? + +items out of scope of purely UID based approach: +- detect message moves between folders +- recovering from UIDVALIDITY change (uw-imap does this a lot) diff --git a/configure.in b/configure.in @@ -1,31 +1,32 @@ AC_INIT(src/isync.h) AM_CONFIG_HEADER(config.h) -AM_INIT_AUTOMAKE(isync, 0.9.2) +AM_INIT_AUTOMAKE(isync, 1.0.0alpha) AM_MAINTAINER_MODE AM_PROG_CC_STDC if test "$GCC" = yes; then - CFLAGS="$CFLAGS -pipe -W -Wall -Wshadow -Wmissing-prototypes" + CFLAGS="$CFLAGS -pipe -W -Wall -Wshadow -Wstrict-prototypes" fi -AC_CHECK_FUNCS(getopt_long) +AC_CHECK_FUNCS(vasprintf) -AC_CHECK_LIB(socket, socket) -AC_CHECK_LIB(nsl, inet_ntoa) +AC_CHECK_LIB(socket, socket, [SOCK_LIBS="-lsocket"]) +AC_CHECK_LIB(nsl, inet_ntoa, [SOCK_LIBS="$SOCK_LIBS -lnsl"]) +AC_SUBST(SOCK_LIBS) ssl=false AC_ARG_WITH(ssl, - [ --with-ssl=DIR yes/no/OpenSSL installation root [detect]], + AS_HELP_STRING([--with-ssl=DIR], [yes/no/OpenSSL installation root [detect]]), [ob_cv_with_ssl=$withval]) if test "x$ob_cv_with_ssl" != xno; then if test -d "$ob_cv_with_ssl/lib"; then - CPFLAGS="$CPPFLAGS -I$ob_cv_with_ssl/include" - LDFLAGS="$LDFLAGS -L$ob_cv_with_ssl/lib" + CPFLAGS="$CPPFLAGS -I$ob_cv_with_ssl/include" + LDFLAGS="$LDFLAGS -L$ob_cv_with_ssl/lib" fi AC_CHECK_LIB(crypto, ERR_error_string, [cryptolib=" -lcrypto"]) AC_CHECK_LIB(ssl, SSL_library_init, [ - LIBS="-lssl$cryptolib $LIBS" + SSL_LIBS="-lssl$cryptolib" AC_DEFINE(HAVE_LIBSSL, 1, [Define if you want SSL support]) ssl=true ],[ @@ -34,19 +35,29 @@ if test "x$ob_cv_with_ssl" != xno; then fi ]) fi +AC_SUBST(SSL_LIBS) -AC_CACHE_CHECK(for db_create in -ldb, ac_cv_db_db_create, - [ac_cv_db_dbcreate=no - AC_TRY_LINK([#include <db.h>], - [db_create();],[ac_cv_db_db_create=yes])]) -if test $ac_cv_db_db_create=yes; then - LIBS="$LIBS -ldb" -else - AC_MSG_ERROR([Berkley DB not found. -You must install libdb including the respective development files/headers.]) +AC_CACHE_CHECK([for Berkley DB 4.2], ac_cv_berkdb4, + [ac_cv_berkdb4=no + AC_TRY_LINK([#include <db.h>], + [DB *db; + db->truncate(db, 0, 0, 0); + db->open(db, 0, "foo", "foo", DB_HASH, DB_CREATE, 0)], + [ac_cv_berkdb4=yes])]) +if test "x$ac_cv_berkdb4" = xno; then + AC_MSG_ERROR([Berkley DB 4.2 not found. +You must install libdb4.2 including the respective development files/headers.]) +fi + +AC_ARG_ENABLE(compat, + AS_HELP_STRING([--disable-compat], [don't include isync compatibility wrapper [no]]), + [ob_cv_enable_compat=$enableval]) +if test "x$ob_cv_enable_compat" != xno; then + AC_CHECK_FUNCS(getopt_long) fi +AM_CONDITIONAL(with_compat, test "x$ob_cv_enable_compat" != xno) -AC_OUTPUT(Makefile src/Makefile isync.spec) +AC_OUTPUT(Makefile src/Makefile src/compat/Makefile isync.spec) if $ssl; then AC_MSG_RESULT([ diff --git a/debian/NEWS b/debian/NEWS @@ -1,16 +0,0 @@ -isync (0.8-1) unstable; urgency=low - - IMPORTANT upgrade note: - - This version includes a change to the way the UID for each message is - stored in the local mailbox. You need to remove all the messages in your - local folder if you were previously using another version of isync or else - you will end up with duplicate messages on your IMAP server. - - A suggested upgrade procedure is to use isync version 0.7 to synchronize - any local changes in isync-managed mailboxes with your IMAP server, and - then remove the contents of the local mailboxes, before upgrading to this - version. Then run isync again to pull down the mail again. You must do - this manually, the Debian package will not do this for you. - - -- Joey Hess <joeyh@debian.org> Tue, 29 Oct 2002 13:50:40 -0500 diff --git a/debian/README.Debian b/debian/README.Debian @@ -1,22 +0,0 @@ -A note from isync's web site: - -To use this command effectively, you need a mail client that sets the T -(trashed) flag when it deletes a message from a maildir mailbox, instead of -just removing it altogether. Currently, only Mutt 1.3.27 supports this. Without -such a client, isync will refetch the locally deleted messages from the server -since they will never get expunged. Be sure to put - -set maildir_trash - -in your ~/.muttrc when using Mutt. - -isync can be integrated into Mutt fairly easily with a few hooks: - - folder-hook ~A bind index $ <sync-mailbox> - folder-hook +maildir 'macro index $ "<sync-mailbox>!isync -e maildir\n"' - -where maildir is the name of the local mailbox (or its alias). This works well -so long as you are not modifying the IMAP mailbox outside of Mutt. However, if -you are using another mail program simultaneously Mutt will have the wrong idea -of the local mailbox flags and messages will start disappearing from its index -display (don't worry, they are still on disk). diff --git a/debian/config b/debian/config @@ -1,8 +0,0 @@ -#!/bin/sh -set -e -. /usr/share/debconf/confmodule -if [ "$1" = "configure" -a ! -z "$2" ] && \ - dpkg --compare-versions "$2" lt 0.8; then - db_input critical isync/upgrade_0.8 || true - db_go || true -fi diff --git a/debian/control b/debian/control @@ -4,21 +4,26 @@ Priority: optional Maintainer: Nicolas Boullis <nboullis@debian.org> Uploaders: Nicolas Boullis <nboullis@debian.org>, Theodore Y. Ts'o <tytso@mit.edu> Standards-Version: 3.6.1 -Build-Depends: libssl-dev, debhelper (>= 4.1.16), dpkg-dev (>= 1.9.0), libdb4.0-dev, dpatch +Build-Depends: libssl-dev, debhelper (>= 4.1.16), dpkg-dev (>= 1.9.0), libdb4.2-dev, dpatch Package: isync Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends} Suggests: mutt -Description: Synchronize a local maildir with a remote IMAP4 mailbox - A command line application which synchronizes a local maildir-style - mailbox with a remote IMAP4 mailbox, suitable for use in disconnected - mode. Multiple copies of the remote IMAP4 mailbox can be maintained, - and all flags and messages are synchronized. +Description: Synchronize Maildir and IMAP4 mailboxes + A command line application which synchronizes mailboxes; currently + Maildir and IMAP4 mailboxes are supported. + New messages, message deletions and flag changes can be propagated both ways. + It is useful for working in disconnected mode, such as on a laptop or with a + non-permanent internet collection (dIMAP). . Features: - * Fast mode for fetching new mail only - * Supports imaps: (port 993) TLS/SSL connections - * Supports STARTTLS (RFC2595) for confidentiality - * Supports NAMESPACE (RFC2342) - * Supports CRAM-MD5 (RFC2095) for authentication + * Fine-grained selection of synchronization operations to perform + * Synchronizes single mailboxes or entire mailbox collections + * Partial mirrors possible: keep only the latest messages locally + * Trash functionality: backup messages before removing them + * IMAP features: + * Supports TLS/SSL via imaps: (port 993) and STARTTLS (RFC2595) + * Supports CRAM-MD5 (RFC2195) for authentication + * Supports NAMESPACE (RFC2342) for simplified configuration + * Pipelining for maximum speed (currently only partially implemented) diff --git a/debian/patches/00list b/debian/patches/00list @@ -1 +0,0 @@ -20-cleanup diff --git a/debian/patches/20-cleanup.dpatch b/debian/patches/20-cleanup.dpatch @@ -1,146 +0,0 @@ -#! /bin/sh -e -## 20-cleanup.dpatch by Theodore Ts'o <tytso@mit.edu> -## -## DP: Make sure the database store and the imap database is closed -## DP: if isync is aborted. - -[ -f debian/patches/00patch-opts ] && . debian/patches/00patch-opts -patch_opts="${patch_opts:--f --no-backup-if-mismatch}" - -if [ $# -ne 1 ]; then - echo >&2 "`basename $0`: script expects -patch|-unpatch as argument" - exit 1 -fi -case "$1" in - -patch) patch $patch_opts -p1 < $0;; - -unpatch) patch $patch_opts -p1 -R < $0;; - *) - echo >&2 "`basename $0`: script expects -patch|-unpatch as argument" - exit 1;; -esac - -exit 0 -@DPATCH@ - -Problem description: - ->> If isync dies in the middle of synchronization, or the network ->> connection breaks while it is synchronizing a mailbox, new messages ->> which are downloaded from the IMAP server do not have their UID saved ->> to the maildir directory. This is REALLY, REALLY BAD, because it ->> means that on the next isync, the downloaded messages are re-uploaded ->> to the imap server, resulting in duplicate messages in the IMAP store. ->> ->> This takes means the network download takes longer, and if the network ->> connection is unrealible, it means it's more likely the the IMAP ->> connection will break, resulting in more duplicate messages being ->> uploaded to the servers. (The first time, 14 messages were uploaded ->> to the server. The second time I re-isynced, 65 messages were ->> uploaded to the server, resulting in some 79 duplicate messages that I ->> had to manually weed out. Grr, grr, grr, grr.) - -Problem solution: - -Actually, I managed to figure out the solution a while ago, and got -hung up trying to figure out the right way to submit the patches back -to upstream (there's no mailing list that I can find; so do you just -communicate directly with the developers). Anyway, I got busy and I -never had a chance to send the patches a while ago. - -This patch is not the best, but it does seem to work. Perhaps a -better approach would be to use the more advanced API's available with -berkdb, so you can actually force a sync to the db/dbm files after -the mail message has been downloaded. Fundamentally, that's the -problem. The id has been added to the db file, but the changes don't -get forced out to disk, so in the case of an abnormal termination of -the program, the id's never get written to disk. - -The patch enclosed below solves the problem by establishing a signal -handler, which cleans up in the case of the user typing ^C (after the -network connection has gone away, say because your GSM phone's GPRS -connection has gotten flakey, for example). However, it doesn't solve -the problem in case of an abrupt system crash. In order to address -that problem, the overall program interfaces would have to be changed -to use the newer berkdb interfaces directly, but that would mean -dropping compatibility with the ancient dbm interface. Personally, I -don't think that to be any great loss, but the changes would be much -more invasive, and would require agreement with the upstream -maintainer that this is the right way to go. - -Also, for bonus points, perhaps there should be an inactivity timer so -that isync can automatically figure out when the network connection -has gone away, and can do a clean shutdown and exit automatically, -instead of requiring the user to type ^C. - - - Ted - - -Patched files: src/main.c -=================================================================== -RCS file: isync-0.9.2/src/RCS/main.c,v -retrieving revision 1.3 -diff -u -r1.3 isync-0.9.2/src/main.c ---- isync-0.9.2/src/main.c 2004/01/10 01:13:38 1.3 -+++ isync-0.9.2/src/main.c 2004/01/10 01:14:34 -@@ -35,6 +35,7 @@ - #include <string.h> - #include <ctype.h> - #include <dirent.h> -+#include <signal.h> - - int Quiet; - -@@ -92,6 +93,22 @@ - unsigned int Tag = 0; - char Hostname[256]; - int Verbose = 0; -+mailbox_t *CleanupMail = 0; -+imap_t *CleanupImap = 0; -+int CleanupValid = 0; -+ -+static void signal_exit(int sig) -+{ -+ info("Abort received\n"); -+ if (CleanupValid) { -+ info("Aborting, cleaning up\n"); -+ if (CleanupMail) -+ maildir_close (CleanupMail); -+ if (CleanupImap) -+ imap_close (CleanupImap); -+ } -+ exit (1); -+} - - static void - version (void) -@@ -319,6 +336,10 @@ - usage (1); - } - -+ signal(SIGTERM, signal_exit); -+ signal(SIGHUP, signal_exit); -+ signal(SIGINT, signal_exit); -+ - gethostname (Hostname, sizeof (Hostname)); - - load_config (config, &o2o); -@@ -410,6 +431,9 @@ - ret = 1; - break; - } -+ CleanupValid = 1; -+ CleanupMail = mail; -+ CleanupImap = imap; - - info ("Synchronizing\n"); - i = (delete || box->delete) ? SYNC_DELETE : 0; -@@ -460,6 +484,8 @@ - - } while (0); - -+ CleanupValid = 0; -+ - /* we never sync the same mailbox twice, so close it now */ - if (mail) - maildir_close (mail); - diff --git a/debian/patches/30-async-imap.dpatch b/debian/patches/30-async-imap.dpatch @@ -1,458 +0,0 @@ -#!/bin/sh -e -## 30-aysnc-imap.dpatch by Theodore Y. Ts'o <tytso@mit.edu> -## -## DP: Add the beginnings of asynchronous IMAP support. So far, we only -## DP: support asynchronous flag setting, since that's the easist. -## DP: Eventually we need to support asynchronous message fetches and -## DP: uploads. - -if [ $# -ne 1 ]; then - echo >&2 "`basename $0`: script expects -patch|-unpatch as argument" - exit 1 -fi - -[ -f debian/patches/00patch-opts ] && . debian/patches/00patch-opts -patch_opts="${patch_opts:--f --no-backup-if-mismatch}" - -case "$1" in - -patch) patch $patch_opts -p1 < $0;; - -unpatch) patch $patch_opts -p1 -R < $0;; - *) - echo >&2 "`basename $0`: script expects -patch|-unpatch as argument" - exit 1;; -esac - -exit 0 - -@DPATCH@ -diff -urNad /usr/projects/isync/SF-cvs/isync/src/imap.c isync/src/imap.c ---- /usr/projects/isync/SF-cvs/isync/src/imap.c 2004-01-15 14:24:40.000000000 -0500 -+++ isync/src/imap.c 2004-01-15 20:36:15.000000000 -0500 -@@ -3,6 +3,7 @@ - * isync - IMAP4 to maildir mailbox synchronizer - * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> - * Copyright (C) 2002-2003 Oswald Buddenhagen <ossi@users.sf.net> -+ * Copyright (C) 2004 Theodore Ts'o <tytso@alum.mit.edu> - * - * 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 -@@ -35,13 +36,33 @@ - #include <string.h> - #include <ctype.h> - #include <sys/socket.h> -+#include <sys/ioctl.h> - #include <netinet/in.h> -+#include <netinet/tcp.h> - #include <arpa/inet.h> - #include <netdb.h> - #if HAVE_LIBSSL - # include <openssl/err.h> - #endif - -+struct imap_cmd { -+ unsigned int tag; -+ char *cmd; -+ int flags; -+ int response; -+ struct imap_cmd *next; -+ int (*complete_fn) (imap_t *imap, struct imap_cmd * cmd); -+}; -+ -+#define IMAP_FLAG_DONE 0x0001 -+ -+static struct imap_cmd *in_progress = NULL; -+static int num_in_progress = 0; -+int max_in_progress_high = 50; -+int max_in_progress_low = 10; -+ -+static struct imap_cmd *get_cmd_result(imap_t *imap); -+ - const char *Flags[] = { - "\\Seen", - "\\Answered", -@@ -199,6 +220,22 @@ - return write (sock->fd, buf, len); - } - -+static int -+socket_pending(Socket_t *sock) -+{ -+ int num = -1; -+ -+ if (ioctl(sock->fd, FIONREAD, &num) < 0) -+ return -1; -+ if (num > 0) -+ return num; -+#if HAVE_LIBSSL -+ if (sock->use_ssl) -+ return SSL_pending (sock->ssl); -+#endif -+ return 0; -+} -+ - static void - socket_perror (const char *func, Socket_t *sock, int ret) - { -@@ -301,16 +338,20 @@ - } - - static int --parse_fetch (imap_t * imap, list_t * list) -+parse_fetch (imap_t * imap, char *cmd) - { -- list_t *tmp; -+ list_t *tmp, *list; - unsigned int uid = 0; - unsigned int mask = 0; - unsigned int size = 0; - message_t *cur; - -- if (!is_list (list)) -+ list = parse_list (cmd, 0); -+ -+ if (!is_list (list)) { -+ free_list(list); - return -1; -+ } - - for (tmp = list->child; tmp; tmp = tmp->next) - { -@@ -325,6 +366,7 @@ - if (uid < imap->minuid) - { - /* already saw this message */ -+ free_list(list); - return 0; - } - else if (uid > imap->maxuid) -@@ -387,6 +429,7 @@ - cur->flags = mask; - cur->size = size; - -+ free_list(list); - return 0; - } - -@@ -415,39 +458,121 @@ - } - } - --static int --imap_exec (imap_t * imap, const char *fmt, ...) -+static void print_imap_command(const char *cmd, FILE *f) -+{ -+ if (strncmp(cmd, "LOGIN", 5)) -+ fputs(cmd, f); -+ else -+ fputs("LOGIN USERNAME PASSWORD", f); -+} -+ -+static struct imap_cmd *issue_imap_cmd(imap_t *imap, -+ const char *fmt, ...) - { - va_list ap; -- char tmp[256]; -- char buf[256]; -- char *cmd; -- char *arg; -- char *arg1; -- config_t *box; -+ char tmp[1024]; -+ char buf[1024]; -+ struct imap_cmd *cmd; - int n; - -+ cmd = malloc(sizeof(struct imap_cmd)); -+ if (!cmd) -+ return NULL; -+ -+ cmd->tag = ++Tag; -+ cmd->flags = 0; -+ cmd->response = 0; -+ cmd->complete_fn = 0; -+ - va_start (ap, fmt); - vsnprintf (tmp, sizeof (tmp), fmt, ap); - va_end (ap); - -- snprintf (buf, sizeof (buf), "%d %s\r\n", ++Tag, tmp); -+ cmd->cmd = malloc(strlen(tmp)+1); -+ if (cmd->cmd) -+ strcpy(cmd->cmd, tmp); -+ -+ snprintf (buf, sizeof (buf), "%d %s\r\n", cmd->tag, tmp); - if (Verbose) { -- printf (">>> %s", buf); -+ if (num_in_progress) -+ printf("(%d in progress) ", num_in_progress); -+ printf(">>> %d ", cmd->tag); -+ print_imap_command(tmp, stdout); -+ fputc('\n', stdout); - fflush (stdout); - } - n = socket_write (imap->sock, buf, strlen (buf)); - if (n <= 0) - { - socket_perror ("write", imap->sock, n); -- return -1; -+ free(cmd); -+ return NULL; - } -+ cmd->next = in_progress; -+ in_progress = cmd; -+ num_in_progress++; -+ if ((num_in_progress > max_in_progress_high) || -+ socket_pending(imap->sock)) { -+ while ((num_in_progress > max_in_progress_low) || -+ socket_pending(imap->sock)) { -+ if (Verbose && socket_pending(imap->sock)) -+ printf("(Socket input pending)\n"); -+ get_cmd_result(imap); -+ } -+ } -+ return cmd; -+} -+ -+static struct imap_cmd *find_imap_cmd(unsigned int tag) -+{ -+ struct imap_cmd *cmd, *prev; -+ -+ for (prev=NULL, cmd=in_progress; cmd; cmd = cmd->next) { -+ if (tag == cmd->tag) { -+ return cmd; -+ } -+ prev = cmd; -+ } -+ return NULL; -+} -+ -+static void dequeue_imap_cmd(unsigned int tag) -+{ -+ struct imap_cmd *cmd, *prev; -+ -+ for (prev=NULL, cmd=in_progress; cmd; cmd = cmd->next) { -+ if (tag != cmd->tag) { -+ prev = cmd; -+ continue; -+ } -+ if (prev) -+ prev->next = cmd->next; -+ else -+ in_progress = cmd->next; -+ cmd->next = 0; -+ if (cmd->cmd) -+ free(cmd->cmd); -+ cmd->cmd = 0; -+ free(cmd); -+ break; -+ } -+} -+ -+static struct imap_cmd *get_cmd_result(imap_t *imap) -+{ -+ char *cmd; -+ char *arg; -+ char *arg1; -+ config_t *box; -+ int n; -+ unsigned int tag; -+ struct imap_cmd *cmdp; - - for (;;) - { - next: - if (buffer_gets (imap->buf, &cmd)) -- return -1; -+ return NULL; - - arg = next_arg (&cmd); - if (*arg == '*') -@@ -456,7 +581,7 @@ - if (!arg) - { - fprintf (stderr, "IMAP error: unable to parse untagged response\n"); -- return -1; -+ return NULL; - } - - if (!strcmp ("NAMESPACE", arg)) -@@ -528,23 +653,14 @@ - imap->recent = atoi (arg); - else if (!strcmp ("FETCH", arg1)) - { -- list_t *list; -- -- list = parse_list (cmd, 0); -- -- if (parse_fetch (imap, list)) -- { -- free_list (list); -- return -1; -- } -- -- free_list (list); -+ if (parse_fetch (imap, cmd)) -+ return NULL; - } - } - else - { - fprintf (stderr, "IMAP error: unable to parse untagged response\n"); -- return -1; -+ return NULL; - } - } - #if HAVE_LIBSSL -@@ -555,7 +671,7 @@ - if (!imap->cram) - { - fprintf (stderr, "IMAP error, not doing CRAM-MD5 authentication\n"); -- return -1; -+ return NULL; - } - resp = cram (cmd, imap->box->user, imap->box->pass); - -@@ -568,34 +684,94 @@ - if (n <= 0) - { - socket_perror ("write", imap->sock, n); -- return -1; -+ return NULL; - } - n = socket_write (imap->sock, "\r\n", 2); - if (n <= 0) - { - socket_perror ("write", imap->sock, n); -- return -1; -+ return NULL; - } - imap->cram = 0; - } - #endif -- else if ((size_t) atol (arg) != Tag) -- { -- fprintf (stderr, "IMAP error: wrong tag\n"); -- return -1; -- } -- else -- { -- arg = next_arg (&cmd); -- parse_response_code (imap, cmd); -- if (!strcmp ("OK", arg)) -- return 0; -- return -1; -+ else { -+ tag = (unsigned int) atol (arg); -+ cmdp = find_imap_cmd(tag); -+ if (!cmdp) { -+ fprintf(stderr, "IMAP error: sent unknown tag: %u\n", -+ tag); -+ return NULL; -+ } -+ arg = next_arg (&cmd); -+ if (strncmp("OK", arg, 2)) { -+ if (cmdp->cmd) { -+ fputc('\'', stderr); -+ print_imap_command(cmdp->cmd, stderr); -+ fputc('\'', stderr); -+ } else -+ fprintf(stderr, "tag %u", tag); -+ fprintf(stderr, " returned an error (%s): %s\n", -+ arg, cmd ? cmd : ""); -+ cmdp->response = -1; -+ } -+ parse_response_code (imap, cmd); -+ num_in_progress--; -+ cmdp->flags |= IMAP_FLAG_DONE; -+ if (Verbose) -+ printf("Tag %u completed with response %d\n", -+ cmdp->tag, cmdp->response); -+ return cmdp; - } - } - /* not reached */ - } - -+static void flush_imap_cmds(imap_t *imap) -+{ -+ struct imap_cmd *cmdp; -+ -+ while (num_in_progress) { -+ if (in_progress && in_progress->flags & IMAP_FLAG_DONE) { -+ dequeue_imap_cmd(in_progress->tag); -+ continue; -+ } -+ cmdp = get_cmd_result(imap); -+ if (!cmdp) -+ printf("Error trying to flush pending imap cmds\n"); -+ } -+} -+ -+static int -+imap_exec (imap_t * imap, const char *fmt, ...) -+{ -+ va_list ap; -+ char tmp[1024]; -+ struct imap_cmd *cmdp, *waitp; -+ int result; -+ -+ va_start (ap, fmt); -+ vsnprintf (tmp, sizeof (tmp), fmt, ap); -+ va_end (ap); -+ -+ cmdp = issue_imap_cmd(imap, "%s", tmp); -+ if (!cmdp) -+ return -1; -+ -+ if (cmdp->flags & IMAP_FLAG_DONE) -+ return cmdp->response; -+ -+ do { -+ waitp = get_cmd_result(imap); -+ } while (waitp->tag != cmdp->tag); -+ -+ result = cmdp->response; -+ dequeue_imap_cmd(cmdp->tag); -+ -+ return cmdp->response; -+ -+} -+ - #ifdef HAVE_LIBSSL - static int - start_tls (imap_t *imap, config_t * cfg) -@@ -1039,6 +1215,7 @@ - size_t n; - char buf[1024]; - -+ flush_imap_cmds(imap); - send_server (imap->sock, "UID FETCH %d BODY.PEEK[]", uid); - - for (;;) -@@ -1160,7 +1337,9 @@ - (buf[0] != 0) ? " " : "", Flags[i]); - } - -- return imap_exec (imap, "UID STORE %d +FLAGS.SILENT (%s)", uid, buf); -+ if (issue_imap_cmd(imap, "UID STORE %d +FLAGS.SILENT (%s)", uid, buf)) -+ return 0; -+ return -1; - } - - int -@@ -1249,6 +1428,7 @@ - strcat (flagstr,") "); - } - -+ flush_imap_cmds(imap); - send_server (imap->sock, "APPEND %s%s %s{%d}", - imap->prefix, imap->box->box, flagstr, len + extra); - -@@ -1341,6 +1521,7 @@ - } - - /* didn't receive an APPENDUID */ -+ flush_imap_cmds(imap); - send_server (imap->sock, - "UID SEARCH HEADER X-TUID %08lx%05lx%04x", - tv.tv_sec, tv.tv_usec, pid); diff --git a/debian/po/POTFILES.in b/debian/po/POTFILES.in @@ -1 +0,0 @@ -[type: gettext/rfc822deb] templates diff --git a/debian/po/fr.po b/debian/po/fr.po @@ -1,74 +0,0 @@ -# -# Translators, if you are not familiar with the PO format, gettext -# documentation is worth reading, especially sections dedicated to -# this format, e.g. by running: -# info -n '(gettext)PO Files' -# info -n '(gettext)Header Entry' -# -# Some information specific to po-debconf are available at -# /usr/share/doc/po-debconf/README-trans -# or http://www.debian.org/intl/l10n/po-debconf/README-trans -# -# Developers do not need to manually edit POT or PO files. -# -msgid "" -msgstr "" -"Project-Id-Version: isync 0.9.1-1\n" -"POT-Creation-Date: 2003-10-14 21:55+0200\n" -"PO-Revision-Date: 2003-10-27 11:52+0100\n" -"Last-Translator: Christian Perrier <bubulle@debian.org>\n" -"Language-Team: French <debian-l10n-french@lists.debian.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=iso-8859-15\n" -"Content-Transfer-Encoding: 8bit\n" - -#. Description -#: ../templates:4 -msgid "Abort isync upgrade" -msgstr "Interrompre la mise à jour d'isync" - -#. Description -#: ../templates:4 -msgid "" -"You are upgrading from an older version of isync that stored the UID of each " -"message in a way that is not compatable with the new version. You need to " -"remove all the messages in local folders downloaded with the old version of " -"isync. Otherwise isync will get confused and upload duplicate messages to " -"the IMAP server." -msgstr "" -"Vous mettez isync à jour à partir d'une version qui utilise une méthode de " -"stockage des identifiants des messages incompatible avec la nouvelle " -"version. Il faut supprimer des répertoires locaux tous les messages " -"téléchargés avec l'ancienne version ; sinon, isync fonctionnera " -"incorrectement et enverra au serveur IMAP des doublons des messages." - -#. Description -#: ../templates:4 -msgid "" -"A suggested upgrade procedure is to use the isync version 0.7 to synchronize " -"any local changes in isync-managed mailboxes with your IMAP server (if there " -"are any local changes to synchronise), and then remove the contents of the " -"local mailboxes, before upgrading to version 0.8 or above. Then run isync " -"again to pull down the mail again. You must do this manually; the Debian " -"package will not do this for you." -msgstr "" -"La méthode suggérée pour la mise à jour est la suivante : en utilisant " -"la version 0.7 d'isync, synchronisez avec le serveur IMAP les éventuelles modifications locales " -"des boîtes aux lettres gérées par isync ; puis " -"supprimez le contenu des boîtes aux lettres locales. Ensuite, effectuez la " -"mise à jour vers une version supérieure ou égale à 0.8. Enfin, utilisez à " -"nouveau isync pour récupérer les courriels. Vous devez effectuer cette " -"opération vous-même : le paquet Debian ne la fera pas automatiquement." - -#. Description -#: ../templates:4 -msgid "" -"If you want, the upgrade of isync can be aborted to let you deal with this " -"issue. Or you can just suspend the upgrade or switch to a different virtual " -"console to take care of it. Do not continue past this point before manually " -"resolving this issue!" -msgstr "" -"Si vous le souhaitez, la mise à jour d'isync peut être interrompue pour vous " -"permettre d'effectuer cette opération. Vous pouvez également basculer vers " -"une autre console virtuelle pour vous en occuper, puis reprendre la mise à jour. Ne continuez pas " -"sans faire cette correction." diff --git a/debian/po/templates.pot b/debian/po/templates.pot @@ -1,60 +0,0 @@ -# -# Translators, if you are not familiar with the PO format, gettext -# documentation is worth reading, especially sections dedicated to -# this format, e.g. by running: -# info -n '(gettext)PO Files' -# info -n '(gettext)Header Entry' -# -# Some information specific to po-debconf are available at -# /usr/share/doc/po-debconf/README-trans -# or http://www.debian.org/intl/l10n/po-debconf/README-trans -# -# Developers do not need to manually edit POT or PO files. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2003-10-14 21:55+0200\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" -"Language-Team: LANGUAGE <LL@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=CHARSET\n" -"Content-Transfer-Encoding: 8bit\n" - -#. Description -#: ../templates:4 -msgid "Abort isync upgrade" -msgstr "" - -#. Description -#: ../templates:4 -msgid "" -"You are upgrading from an older version of isync that stored the UID of each " -"message in a way that is not compatable with the new version. You need to " -"remove all the messages in local folders downloaded with the old version of " -"isync. Otherwise isync will get confused and upload duplicate messages to " -"the IMAP server." -msgstr "" - -#. Description -#: ../templates:4 -msgid "" -"A suggested upgrade procedure is to use the isync version 0.7 to synchronize " -"any local changes in isync-managed mailboxes with your IMAP server (if there " -"are any local changes to synchronise), and then remove the contents of the " -"local mailboxes, before upgrading to version 0.8 or above. Then run isync " -"again to pull down the mail again. You must do this manually; the Debian " -"package will not do this for you." -msgstr "" - -#. Description -#: ../templates:4 -msgid "" -"If you want, the upgrade of isync can be aborted to let you deal with this " -"issue. Or you can just suspend the upgrade or switch to a different virtual " -"console to take care of it. Do not continue past this point before manually " -"resolving this issue!" -msgstr "" diff --git a/debian/preinst b/debian/preinst @@ -1,19 +0,0 @@ -#!/bin/sh -set -e - -if [ "$1" = "upgrade" ] && dpkg --compare-versions "$2" lt "0.8"; then - # Do not do debconf stuff if debconf is not there. - # I don't want to have to pre-depend on debconf. - if [ -e /usr/share/debconf/confmodule ]; then - . /usr/share/debconf/confmodule - db_get isync/upgrade_0.8 - if [ "$RET" = true ]; then - echo "Aborting isync upgrade at your request so you can manually resolve upgrade issue." >&2 - exit 1 - fi - else - echo "WARNING: Read NEWS.Debian file about manual upgrade issues from isync 0.7." >&2 - fi -fi - -#DEBHELPER# diff --git a/debian/rules b/debian/rules @@ -41,9 +41,8 @@ binary-arch: build install dh_testroot dh_installchangelogs ChangeLog dh_installdocs AUTHORS NEWS README TODO - dh_installexamples isyncrc.sample + dh_installexamples src/mbsyncrc.sample src/compat/isyncrc.sample dh_installman - dh_installdebconf dh_strip dh_compress dh_fixperms diff --git a/debian/templates b/debian/templates @@ -1,21 +0,0 @@ -Template: isync/upgrade_0.8 -Type: boolean -Default: false -_Description: Abort isync upgrade - You are upgrading from an older version of isync that stored the UID of - each message in a way that is not compatable with the new version. You - need to remove all the messages in local folders downloaded with the old - version of isync. Otherwise isync will get confused and upload duplicate - messages to the IMAP server. - . - A suggested upgrade procedure is to use the isync version 0.7 to - synchronize any local changes in isync-managed mailboxes with your IMAP - server (if there are any local changes to synchronise), and then remove - the contents of the local mailboxes, before upgrading to version 0.8 or - above. Then run isync again to pull down the mail again. You must do this - manually; the Debian package will not do this for you. - . - If you want, the upgrade of isync can be aborted to let you deal with this - issue. Or you can just suspend the upgrade or switch to a different - virtual console to take care of it. Do not continue past this point before - manually resolving this issue! diff --git a/isync.1 b/isync.1 @@ -1,344 +0,0 @@ -.ig -\" isync - IMAP4 to maildir mailbox synchronizer -\" Copyright (C) 2000-2002 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 -\" -\" As a special exception, isync may be linked with the OpenSSL library, -\" despite that library's more restrictive license. -.. -.TH isync 1 "2002 Dec 22" -.. -.SH NAME -isync - synchronize IMAP4 and maildir mailboxes -.. -.SH SYNOPSIS -\fBisync\fR [\fIoptions...\fR] \fImailbox\fR [\fImailbox ...\fR] -.br -\fBisync\fR [\fIoptions...\fR] \fI-a\fR -.br -\fBisync\fR [\fIoptions...\fR] \fI-l\fR -.. -.SH DESCRIPTION -\fBisync\fR 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 \fIfile\fR. -By default, the configuration is read from ~/.isyncrc if it exists. -.TP -\fB-1\fR, \fB--one-to-one\fR -Instead of using the mailbox specifications in ~/.isyncrc, isync will pick up -all mailboxes from the local directory and remote folder and map them 1:1 -onto each other according to their names. -.TP -\fB-I\fR, \fB--inbox\fR \fImailbox\fR -Exception to the 1:1 mapping created by -1: the special IMAP mailbox \fIINBOX\fR -is mapped to the local \fImailbox\fR (relative to the maildir). -.TP -\fB-a\fR, \fB--all\fR -Synchronize all mailboxes (either specified in ~/.isyncrc or determined by the -1:1 mapping). -.TP -\fB-l\fR, \fB--list\fR -Don't synchronize anything, but list all mailboxes and exit. -.TP -\fB-L\fR, \fB--create-local\fR -Automatically create the local maildir-style mailbox if it doesn't already -exist. -.TP -\fB-R\fR, \fB--create-remote\fR -Automatically create the remote IMAP mailbox if it doesn't already exist. -.TP -\fB-C\fR, \fB--create\fR -Automatically create any mailboxes if they don't already exist. -This is simply a combination of -L and -R. -.TP -\fB-d\fR, \fB--delete\fR -Causes \fBisync\fR to delete messages from the local maildir mailbox -which do not exist on the IMAP server. By default, \fIdead\fR messages -are \fBnot\fR deleted. -.TP -\fB-e\fR, \fB--expunge\fR -Causes \fBisync\fR to permanently remove all messages marked for deletion -in both the local maildir mailbox and the remote IMAP mailbox. By default, -\fIdeleted\fR messages are \fBnot\fR expunged. -.TP -\fB-f\fR, \fB--fast\fR -Causes \fBisync\fR 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. -.TP -\fB-h\fR, \fB--help\fR -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-q\fR, \fB--quiet\fR -Suppress informational messages. -If specified twice, suppress warning messages as well. -.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 \fB[\fRimaps:\fB]\fR\fIhost\fR -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 -\fB-M\fR, \fB--maildir\fR \fIdir\fR -Specifies the location for your local mailboxes. -.TP -\fB-F\fR, \fB--folder\fR \fIfolder\fR/ -Specifies the location for your remote mailboxes. -.TP -\fB-v\fR, \fB--version\fR -Displays \fBisync\fR version information. -.TP -\fB-V\fR, \fB--verbose\fR -Enables \fIverbose\fR mode, which displays the IMAP4 network traffic. -.. -.SH CONFIGURATION -\fBisync\fR reads \fI~/.isyncrc\fR 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 \fIMailbox\fR command, apply to this mailbox only. -.. -.TP -\fBHost\fR \fB[\fRimaps:\fB]\fR\fIname\fR -Defines the DNS name or IP address of the IMAP server. If the hostname is -prefixed with \fIimaps:\fR the connection is assumed to be a SSL connection -to port 993 (though you can change this by placing a \fBPort\fR command -\fBafter\fR the \fBHost\fR command. Note that some servers support SSL on -the default port 143. \fBisync\fR will always attempt to use SSL if available. -.. -.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 \fIusername\fR on the IMAP server. -Note that this option is \fBNOT\fR required. -If no password is specified in the configuration file, \fBisync\fR -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. -.. -.TP -\fBCopyDeletedTo\fR \fImailbox\fR -Specifies the remote IMAP mailbox to copy deleted messages prior to -expunging (Default: none). -.. -.TP -\fBDelete\fR \fIyes\fR|\fIno\fR -Specifies whether messages in the local copy of the mailbox which don't -exist on the server are automatically deleted. (Default: no). -\fBNOTE:\fR The \fI-d\fR command line option overrides this setting when -set to \fIno\fR. -.. -.TP -\fBExpunge\fR \fIyes\fR|\fIno\fR -Specifies whether deleted messages are expunged by default (Default: no). -\fBNOTE:\fR The \fI-e\fR command line option overrides this setting when -set to \fIno\fR. -.. -.TP -\fBMailDir\fR \fIdirectory\fR -Specifies the location of your local mailboxes if a relative path is -specified in a \fIMailbox\fR command (Default: \fI~\fR). -\fBNOTE:\fR This directive is only meaningful in the \fIglobal\fR -section (see below). -.. -.TP -\fBFolder\fR \fIdirectory\fR/ -Specifies the location of your IMAP mailboxes -specified in \fIBox\fR commands (Default: \fI""\fR). -\fBNOTE:\fR You \fBmust\fR append the hierarchy delimiter (usually -a slash) to this specification. -\fBNOTE 2:\fR This directive is only meaningful in the \fIglobal\fR -section (see below). -.. -.TP -\fBMaxMessages\fR \fIcount\fR -Sets the number of messages \fBisync\fR should keep in a mailbox. -This is useful for mailboxes where you keep a complete archive on the -server, but want to mirror only the last messages (for instance, for mailing -lists.) -The messages that were the first to arrive in the mailbox (independent of the -actual date of the message) will automatically be deleted if you -pass \fBisync\fR the delete (-d, --delete) flag. -Messages that are flagged (marked as important) will not be automatically -deleted. -If \fIcount\fR is 0, the maximum number of messages is \fBunlimited\fR -(Default: 0). -.. -.TP -\fBMaxSize\fR \fIbytes\fR -Sets a threshold for the maximum message size (in bytes) for which \fBisync\fR -should transfer over the wire. This is useful for weeding out messages with -large attachments. If \fIbytes\fR is 0, the maximum file size is \fBunlimited\fR. -.. -.TP -\fBTunnel\fR \fIcommand\fR -Specify a command to run to establish a connection rather than opening a TCP -socket. This allows you to run an IMAP session over an SSH tunnel, for -example. -.TP -\fBUseNamespace\fR \fIyes\fR|\fIno\fR -Selects whether \fBisync\fR should select mailboxes using the namespace given -by the NAMESPACE command. This is useful with broken IMAP servers. (Default: -\fIyes\fR) -.. -.TP -\fBRequireCRAM\fR \fIyes\fR|\fIno\fR -If set to \fIyes\fR, \fBisync\fR will require that the server accept CRAM-MD5 -intead of PLAIN to authenticate the user. -.. -.TP -\fBRequireSSL\fR \fIyes\fR|\fIno\fR -\fBisync\fR will abort the connection if a TLS/SSL session to the IMAP -server can not be established. (Default: \fIyes\fR) -.. -.TP -\fBCertificateFile\fR \fIpath\fR -File containing X.509 CA certificates used to verify server identities. -.. -.TP -\fBUseSSLv2\fR \fIyes\fR|\fIno\fR -Should \fBisync\fR use SSLv2 for communication with the IMAP server over SSL? -(Default: \fIyes\fR if the imaps port is used, otherwise \fIno\fR) -.. -.TP -\fBUseSSLv3\fR \fIyes\fR|\fIno\fR -Should \fBisync\fR use SSLv3 for communication with the IMAP server over SSL? -(Default: \fIyes\fR if the imaps port is used, otherwise \fIno\fR) -.. -.TP -\fBUseTLSv1\fR \fIyes\fR|\fIno\fR -Should \fBisync\fR use TLSv1 for communication with the IMAP server over SSL? -(Default: \fIyes\fR) -.. -.TP -\fBOneToOne\fR -\fBisync\fR will ignore any \fIMailbox\fR specifications and instead pick up -all mailboxes from the local \fIMailDir\fR and remote \fIFolder\fR and map -them 1:1 onto each other according to their names. -\fBNOTE:\fR This directive is only meaningful in the \fIglobal\fR -section (see below). -.. -.TP -\fBInbox\fR \fImailbox\fR -Exception to the OneToOne mapping: the special IMAP mailbox \fIINBOX\fR -is mapped to the local \fImailbox\fR (relative to the \fIMailDir\fR). -\fBNOTE:\fR This directive is only meaningful in the \fIglobal\fR -section (see below). -.. -.P -Configuration commands that appear prior to the first \fBMailbox\fR -command are considered to be \fIglobal\fR -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 \fBUser\fR command before -the first \fBMailbox\fR command, and then leave out the \fBUser\fR command -in the sections for each mailbox. -\fBisync\fR will then use the global value by default. -.. -.SH MAIL USER AGENT INTERACTION -To use \fBisync\fR effectively, you need a mail client that sets the T -(trashed) flag when it deletes a message from a maildir mailbox, instead of -just removing it altogether. Without such a client, \fBisync\fR will refetch the -locally deleted messages from the server since they will never get expunged. -Mutt (starting with version 1.3.27) is known to support this. Be sure to put -.IP "" 2 -set maildir_trash -.PP -in your ~/.muttrc when using Mutt. -.br -\fBisync\fR can be integrated into Mutt fairly easily with a few hooks: -.IP "" 2 -.nf -folder-hook ~A bind index $ <sync-mailbox> -.br -folder-hook +\fImdir\fR 'macro index $ "<sync-mailbox>!isync -e \fImdir\fR\\n"' -.fi -.PP -where \fImdir\fR is the name of the local mailbox (or its \fIalias\fR). -This works well so long as you are not modifying the IMAP mailbox outside of -Mutt. However, if you are using another mail program simultaneously Mutt -will have the wrong idea of the local mailbox flags and messages will start -disappearing from its index display (don't worry, they are still on disk). -.. -.SH FILES -.TP -.B ~/.isyncrc -Default configuration file -.. -.SH BUGS -No support for IMAP subfolders. -.P -\fBisync\fR does not use NFS-safe locking. It will correctly prevent -concurrent synchronization of a mailbox on the same host, but not across NFS. -.P -When synchronizing multiple mailboxes on the same IMAP server, it is not -possible to select different SSL options for each mailbox. Only the options -from the first mailbox are applied since the SSL session is reused. -.P -If new mail arrives in the IMAP mailbox after \fBisync\fR -has retrieved the initial message list, the new mail will not be fetched -until the next time \fBisync\fR is invoked. -.P -It is currently impossible to unset the \\Flagged attribute of a message -once it is set. It has to be manually unset everywhere since isync -doesn't have enough information to know which was the last status of the -message. -.P -The ndbm database created for each mailbox is not portable across different -architectures. It currently stores the UID in host byte order. -.P -The configuration file takes precedence over command line options. -.br -Use -c /dev/null to work around. -.SH SEE ALSO -mutt(1), maildir(5) -.P -Up to date information on \fBisync\fR can be found at http://isync.sf.net/ -.. -.SH AUTHOR -Written by Michael R. Elkins <me@mutt.org>, -.br -maintained by Oswald Buddenhagen <ossi@users.sf.net>. diff --git a/isync.spec.in b/isync.spec.in @@ -10,16 +10,16 @@ Packager: Oswald Buddenhagen <ossi@users.sf.net> BuildRoot: /var/tmp/%{name}-buildroot %description -isync is a command line utility for synchronizing a remote IMAP mailbox with a -local maildir-style mailbox. This is useful for working in disconnected mode, -such as on a laptop. Modifications made locally and remotely are synchronized -so that no message status flags are lost. +isync is a command line utility which synchronizes mailboxes; currently +Maildir and IMAP4 mailboxes are supported. +New messages, message deletions and flag changes can be propagated both ways. +It is useful for working in disconnected mode, such as on a laptop or with a +non-permanent internet collection (dIMAP). %prep %setup %build ./configure --prefix=/usr -make RPM_OPT_FLAGS="$RPM_OPT_FLAGS" %install make DESTDIR=$RPM_BUILD_ROOT install @@ -28,6 +28,11 @@ make DESTDIR=$RPM_BUILD_ROOT install rm -rf $RPM_BUILD_ROOT %files -%doc AUTHORS COPYING README TODO ChangeLog isyncrc.sample +%doc AUTHORS COPYING NEWS README TODO ChangeLog src/mbsyncrc.sample src/compat/isyncrc.sample /usr/bin/isync +/usr/bin/mbsync +/usr/bin/mdconvert +/usr/bin/get-cert /usr/man/man1/isync.1.gz +/usr/man/man1/mbsync.1.gz +/usr/man/man1/mdconvert.1.gz diff --git a/src/.cvsignore b/src/.cvsignore @@ -1,4 +1,5 @@ .deps Makefile Makefile.in -isync +mbsync +mdconvert diff --git a/src/Makefile.am b/src/Makefile.am @@ -1,5 +1,16 @@ -bin_PROGRAMS = isync -isync_SOURCES = main.c imap.c sync.c maildir.c list.c cram.c config.c dotlock.c -noinst_HEADERS = isync.h dotlock.h -INCLUDES=$(RPM_OPT_FLAGS) -DISTCLEANFILES = *~ +if with_compat +compat_dir = compat +endif +SUBDIRS = $(compat_dir) + +bin_PROGRAMS = mbsync mdconvert + +mbsync_SOURCES = main.c sync.c config.c util.c drv_imap.c drv_maildir.c +mbsync_LDADD = -ldb $(SSL_LIBS) $(SOCK_LIBS) +noinst_HEADERS = isync.h + +mdconvert_SOURCES = mdconvert.c +mdconvert_LDADD = -ldb + +man_MANS = mbsync.1 mdconvert.1 +EXTRA_DIST = mbsyncrc.sample $(man_MANS) diff --git a/src/.cvsignore b/src/compat/.cvsignore diff --git a/src/compat/Makefile.am b/src/compat/Makefile.am @@ -0,0 +1,8 @@ +bin_PROGRAMS = isync + +isync_SOURCES = main.c config.c convert.c util.c +isync_LDADD = -ldb +noinst_HEADERS = isync.h + +man_MANS = isync.1 +EXTRA_DIST = isyncrc.sample $(man_MANS) diff --git a/src/compat/config.c b/src/compat/config.c @@ -0,0 +1,443 @@ +/* + * isync - mbsync wrapper: IMAP4 to maildir mailbox synchronizer + * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> + * Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net> + * + * 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 "isync.h" + +#include <unistd.h> +#include <limits.h> +#include <errno.h> +#include <pwd.h> +#include <sys/types.h> +#include <string.h> +#include <stdlib.h> +#include <stdio.h> + +static char * +my_strndup( const char *s, size_t nchars ) +{ + char *r = nfmalloc( sizeof(char) * (nchars + 1) ); + memcpy( r, s, nchars ); + r[nchars] = 0; + return r; +} + +char * +expand_strdup( const char *s ) +{ + struct passwd *pw; + const char *p, *q; + char *r; + + if (*s == '~') { + s++; + if (!*s) { + p = 0; + q = Home; + } else if (*s == '/') { + p = s + 1; + q = Home; + } else { + if ((p = strchr( s, '/' ))) { + r = my_strndup( s, (int)(p - s) ); + pw = getpwnam( r ); + free( r ); + p++; + } else + pw = getpwnam( s ); + if (!pw) + return 0; + q = pw->pw_dir; + } + nfasprintf( &r, "%s/%s", q, p ? p : "" ); + return r; + } else if (*s != '/' && xmaildir) { + nfasprintf( &r, "%s/%s", xmaildir, s ); + return r; + } else + return nfstrdup( s ); +} + +static int +is_true( const char *val ) +{ + return + !strcasecmp( val, "yes" ) || + !strcasecmp( val, "true" ) || + !strcasecmp( val, "on" ) || + !strcmp( val, "1" ); +} + +void +load_config( const char *path, config_t ***stor ) +{ + config_t **sstor, *cfg; + FILE *fp; + char *p, *cmd, *val; + int line = 0; + char buf[1024]; + + if (!(fp = fopen( path, "r" ))) { + if (errno != ENOENT) + perror( "fopen" ); + return; + } + if (!Quiet && !Debug && !Verbose) + printf( "Reading configuration file %s\n", path ); + buf[sizeof(buf) - 1] = 0; + cfg = &global; + while (fgets( buf, sizeof(buf) - 1, fp )) { + p = buf; + cmd = next_arg( &p ); + val = next_arg( &p ); + line++; + if (!cmd || *cmd == '#') + continue; + if (!val) { + fprintf( stderr, "%s:%d: parameter missing\n", path, line ); + continue; + } + if (!strcasecmp( "Mailbox", cmd )) { + if (o2o) + break; + cfg = **stor = nfmalloc( sizeof(config_t) ); + *stor = &cfg->next; + memcpy( cfg, &global, sizeof(config_t) ); + /* not expanded at this point */ + cfg->path = nfstrdup( val ); + } else if (!strcasecmp( "OneToOne", cmd )) { + if (boxes) { + forbid: + fprintf( stderr, + "%s:%d: keyword '%s' allowed only in global section\n", + path, line, cmd ); + continue; + } + o2o = is_true( val ); + } else if (!strcasecmp( "Maildir", cmd )) { + if (boxes) + goto forbid; + maildir = nfstrdup( val ); + xmaildir = expand_strdup( val ); + } else if (!strcasecmp( "Folder", cmd )) { + if (boxes) + goto forbid; + folder = nfstrdup( val ); + } else if (!strcasecmp( "Inbox", cmd )) { + if (boxes) + goto forbid; + inbox = nfstrdup( val ); + } else if (!strcasecmp( "Host", cmd )) { + if (!memcmp( "imaps:", val, 6 )) { + val += 6; + cfg->use_imaps = 1; + cfg->port = 993; + cfg->use_sslv2 = 1; + cfg->use_sslv3 = 1; + } + cfg->host = nfstrdup( val ); + } else if (!strcasecmp( "User", cmd )) + cfg->user = nfstrdup( val ); + else if (!strcasecmp( "Pass", cmd )) + cfg->pass = nfstrdup( val ); + else if (!strcasecmp ( "Port", cmd )) + cfg->port = atoi( val ); + else if (!strcasecmp ( "Box", cmd )) + cfg->box = nfstrdup( val ); + else if (!strcasecmp ( "Alias", cmd )) { + if (!boxes) { + fprintf( stderr, + "%s:%d: keyword 'Alias' allowed only in mailbox specification\n", + path, line ); + continue; + } + cfg->alias = nfstrdup( val ); + } else if (!strcasecmp( "MaxSize", cmd )) + cfg->max_size = atol( val ); + else if (!strcasecmp ( "MaxMessages", cmd )) + cfg->max_messages = atol( val ); + else if (!strcasecmp ( "UseNamespace", cmd )) + cfg->use_namespace = is_true( val ); + else if (!strcasecmp ( "CopyDeletedTo", cmd )) + cfg->copy_deleted_to = nfstrdup( val ); + else if (!strcasecmp ( "Tunnel", cmd )) + cfg->tunnel = nfstrdup( val ); + else if (!strcasecmp ( "Expunge", cmd )) + cfg->expunge = is_true( val ); + else if (!strcasecmp( "Delete", cmd )) + cfg->delete = is_true( val ); + else if (!strcasecmp( "CertificateFile", cmd )) + cfg->cert_file = expand_strdup( val ); + else if (!strcasecmp( "RequireSSL", cmd )) + cfg->require_ssl = is_true( val ); + else if (!strcasecmp( "UseSSLv2", cmd )) + cfg->use_sslv2 = is_true( val ); + else if (!strcasecmp( "UseSSLv3", cmd )) + cfg->use_sslv3 = is_true( val ); + else if (!strcasecmp( "UseTLSv1", cmd )) + cfg->use_tlsv1 = is_true( val ); + else if (!strcasecmp( "RequireCRAM", cmd )) + cfg->require_cram = is_true( val ); + else if (buf[0]) + fprintf( stderr, "%s:%d: unknown keyword '%s'\n", path, line, cmd ); + } + fclose( fp ); + if (o2o) { + if (!global.host && !global.tunnel) { + fprintf( stderr, "Neither Host nor Tunnel given to OneToOne. Aborting.\n" ); + exit( 1 ); + } + } else + for (sstor = &boxes; (cfg = *sstor); ) { + if (!cfg->host && !cfg->tunnel) { + fprintf( stderr, "Mailbox '%s' has neither Host nor Tunnel. Skipping.\n", + cfg->alias ? cfg->alias : cfg->path ); + if (&cfg->next == *stor) + *stor = sstor; + *sstor = cfg->next; + continue; + } + sstor = &cfg->next; + } +} + +static const char * +tb( int on ) +{ + return on ? "yes" : "no"; +} + +static void +write_imap_server( FILE *fp, config_t *cfg ) +{ + config_t *pbox; + char *p, *p2; + int hl, a1, a2, a3, a4; + char buf[128]; + static int tunnels; + + if (cfg->tunnel) { + nfasprintf( (char **)&cfg->server_name, "tunnel%d", ++tunnels ); + fprintf( fp, "IMAPAccount %s\nTunnel \"%s\"\n", + cfg->server_name, cfg->tunnel ); + } else { + if (sscanf( cfg->host, "%d.%d.%d.%d", &a1, &a2, &a3, &a4 ) == 4) + cfg->server_name = nfstrdup( cfg->host ); + else { + p = strrchr( cfg->host, '.' ); + if (!p) + hl = nfsnprintf( buf, sizeof(buf), "%s", cfg->host ); + else { + hl = nfsnprintf( buf, sizeof(buf), "%.*s", p - cfg->host, cfg->host ); + p2 = strrchr( buf, '.' ); + if (p2) + hl = sprintf( buf, "%s", p2 + 1 ); + } + if (boxes) /* !o2o */ + for (pbox = boxes; pbox != cfg; pbox = pbox->next) + if (!memcmp( pbox->server_name, buf, hl + 1 )) { + nfasprintf( (char **)&cfg->server_name, "%s-%d", buf, ++pbox->servers ); + goto gotsrv; + } + cfg->server_name = nfstrdup( buf ); + cfg->servers = 1; + gotsrv: ; + } + fprintf( fp, "IMAPAccount %s\n", cfg->server_name ); + if (cfg->use_imaps) + fprintf( fp, "Host imaps:%s\n", cfg->host ); + else + fprintf( fp, "Host %s\n", cfg->host ); + fprintf( fp, "Port %d\n", cfg->port ); + } + if (cfg->user) + fprintf( fp, "User %s\n", cfg->user ); + if (cfg->pass) + fprintf( fp, "Pass \"%s\"\n", cfg->pass ); + fprintf( fp, "RequireCRAM %s\nRequireSSL %s\n" + "UseSSLv2 %s\nUseSSLv3 %s\nUseTLSv1 %s\n", + tb(cfg->require_cram), tb(cfg->require_ssl), + tb(cfg->use_sslv2), tb(cfg->use_sslv3), tb(cfg->use_tlsv1) ); + if ((cfg->use_imaps || cfg->use_sslv2 || cfg->use_sslv3 || cfg->use_tlsv1) && + cfg->cert_file) + fprintf( fp, "CertificateFile %s\n", cfg->cert_file ); + fputc( '\n', fp ); +} + +static void +write_imap_store( FILE *fp, config_t *cfg ) +{ + if (cfg->stores > 1) + nfasprintf( (char **)&cfg->store_name, "%s-%d", cfg->server_name, cfg->stores ); + else + cfg->store_name = cfg->server_name; + fprintf( fp, "IMAPStore %s\nAccount %s\n", + cfg->store_name, cfg->server_name ); + if (*folder) + fprintf( fp, "Path \"%s\"\n", folder ); + else + fprintf( fp, "UseNamespace %s\n", tb(cfg->use_namespace) ); + if (inbox) + fprintf( fp, "MapInbox \"%s\"\n", inbox ); + if (cfg->copy_deleted_to) + fprintf( fp, "Trash \"%s\"\n", cfg->copy_deleted_to ); + fputc( '\n', fp ); +} + +static void +write_channel_parm( FILE *fp, config_t *cfg ) +{ + if (cfg->max_size) + fprintf( fp, "MaxSize %d\n", cfg->max_size ); + if (cfg->max_messages) + fprintf( fp, "MaxMessages %d\n", cfg->max_messages ); + if (!cfg->delete) + fputs( "Sync New ReNew Flags\n", fp ); + if (cfg->expunge) + fputs( "Expunge Both\n", fp ); + fputc( '\n', fp ); +} + +static int +mstrcmp( const char *s1, const char *s2 ) +{ + if (s1 == s2) + return 0; + if (!s1 || !s2) + return 1; + return strcmp( s1, s2 ); +} + +void +write_config( int fd ) +{ + FILE *fp; + const char *cn, *scn; + config_t *box, *sbox, *pbox; + + if (!(fp = fdopen( fd, "w" ))) { + perror( "fdopen" ); + return; + } + + fprintf( fp, "SyncState *\n\nMaildirStore local\nPath \"%s/\"\nAltMap %s\n\n", maildir, tb( altmap > 0 ) ); + if (o2o) { + write_imap_server( fp, &global ); + write_imap_store( fp, &global ); + fprintf( fp, "Channel o2o\nMaster :%s:\nSlave :local:\nPattern %%\n", global.store_name ); + write_channel_parm( fp, &global ); + } else { + for (box = boxes; box; box = box->next) { + for (pbox = boxes; pbox != box; pbox = pbox->next) { + if (box->tunnel) { + if (mstrcmp( pbox->tunnel, box->tunnel )) + continue; + } else { + if (mstrcmp( pbox->host, box->host ) || + pbox->use_imaps != box->use_imaps || + pbox->port != box->port) + continue; + } + if (mstrcmp( pbox->user, box->user ) || + mstrcmp( pbox->pass, box->pass )) /* nonsense */ + continue; + if ((box->use_imaps || box->use_sslv2 || + box->use_sslv3 || box->use_tlsv1) && + mstrcmp( pbox->cert_file, box->cert_file )) /* nonsense */ + continue; + if (pbox->use_imaps != box->use_imaps || + pbox->use_sslv2 != box->use_sslv2 || + pbox->use_sslv3 != box->use_sslv3 || + pbox->use_tlsv1 != box->use_tlsv1) + continue; + box->server_name = pbox->server_name; + for (sbox = boxes; sbox != box; sbox = sbox->next) { + if (sbox->server_name != box->server_name || + mstrcmp( sbox->copy_deleted_to, box->copy_deleted_to ) || + (!*folder && sbox->use_namespace != box->use_namespace)) + continue; + box->store_name = sbox->store_name; + goto gotall; + } + box->stores = ++pbox->stores; + goto gotsrv; + } + write_imap_server( fp, box ); + box->stores = 1; + gotsrv: + write_imap_store( fp, box ); + gotall: + if (box->alias) + cn = box->alias; + else { + cn = strrchr( box->path, '/' ); + if (cn) + cn++; + else + cn = box->path; + } + for (sbox = boxes; sbox != box; sbox = sbox->next) { + if (sbox->alias) + scn = sbox->alias; + else { + scn = strrchr( sbox->path, '/' ); + if (scn) + scn++; + else + scn = sbox->path; + } + if (mstrcmp( cn, scn )) + continue; + nfasprintf( (char **)&box->channel_name, "%s-%d", cn, ++sbox->channels ); + goto gotchan; + } + box->channels = 1; + box->channel_name = cn; + gotchan: + fprintf( fp, "Channel %s\nMaster :%s:%s\nSlave :local:%s\n", + box->channel_name, box->store_name, box->box, box->path ); + write_channel_parm( fp, box ); + } + + } + + fclose( fp ); +} + +config_t * +find_box( const char *s ) +{ + config_t *p; + char *t; + + for (p = boxes; p; p = p->next) { + if (!strcmp( s, p->path ) || (p->alias && !strcmp( s, p->alias ))) + return p; + /* check to see if the full pathname was specified on the + * command line. + */ + t = expand_strdup( p->path ); + if (!strcmp( s, t )) { + free( t ); + return p; + } + free( t ); + } + return 0; +} diff --git a/src/compat/convert.c b/src/compat/convert.c @@ -0,0 +1,259 @@ +/* + * isync - mbsync wrapper: IMAP4 to maildir mailbox synchronizer + * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> + * Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net> + * + * 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 "isync.h" + +#include <limits.h> +#include <stdlib.h> +#include <string.h> +#include <dirent.h> +#include <fcntl.h> +#include <stdio.h> +#include <unistd.h> +#include <sys/stat.h> +#include <errno.h> +#include <time.h> + +#include <db.h> + +static const char *subdirs[] = { "cur", "new", "tmp" }; + +static const char Flags[] = { 'D', 'F', 'R', 'S', 'T' }; + +static int +parse_info( const char *s ) +{ + unsigned i; + int flags; + + flags = 0; + if (s && *(s + 1) == '2' && *(s + 2) == ',') + for (s += 3, i = 0; i < as(Flags); i++) + if (strchr( s, Flags[i] )) + flags |= (1 << i); + return flags; +} + +typedef struct { + int uid, flags; +} msg_t; + +static int +compare_uids( const void *l, const void *r ) +{ + return ((msg_t *)l)->uid - ((msg_t *)r)->uid; +} + +static DBT key, value; +static struct flock lck; + +void +convert( config_t *box ) +{ + DIR *d; + struct dirent *e; + char *s, *p, *mboxdir; + FILE *fp; + msg_t *msgs; + DB *db; + int i, ret, fd, uidval, maxuid, bl, uid, rmsgs, nmsgs, uv[2]; + unsigned u; + struct stat sb; + char buf[_POSIX_PATH_MAX], diumname[_POSIX_PATH_MAX], + uvname[_POSIX_PATH_MAX], sname[_POSIX_PATH_MAX], + iuvname[_POSIX_PATH_MAX], imuname[_POSIX_PATH_MAX], + ilname[_POSIX_PATH_MAX], iumname[_POSIX_PATH_MAX]; + + mboxdir = expand_strdup( box->path ); + nfsnprintf( iuvname, sizeof(iuvname), "%s/isyncuidvalidity", mboxdir ); + nfsnprintf( diumname, sizeof(iumname), "%s/.isyncuidmap.db", mboxdir ); + nfsnprintf( uvname, sizeof(uvname), "%s/.uidvalidity", mboxdir ); + if (stat( iuvname, &sb )) { + if (!stat( diumname, &sb )) + altmap++; + else if (!stat( uvname, &sb )) + altmap--; + err1: + free( mboxdir ); + return; + } + for (i = 0; i < 3; i++) { + nfsnprintf( buf, sizeof(buf), "%s/%s", mboxdir, subdirs[i] ); + if (stat( buf, &sb )) { + fprintf( stderr, "ERROR: stat %s: %s (errno %d)\n", buf, + strerror(errno), errno ); + fprintf( stderr, + "ERROR: %s does not appear to be a valid maildir style mailbox\n", + mboxdir ); + goto err1; + } + } + nfsnprintf( iumname, sizeof(iumname), "%s/isyncuidmap.db", mboxdir ); + nfsnprintf( imuname, sizeof(imuname), "%s/isyncmaxuid", mboxdir ); + nfsnprintf( ilname, sizeof(ilname), "%s/isynclock", mboxdir ); + nfsnprintf( sname, sizeof(sname), "%s/.mbsyncstate", mboxdir ); + + if ((fd = open( ilname, O_WRONLY|O_CREAT, 0600 )) < 0) { + perror( ilname ); + goto err1; + } +#if SEEK_SET != 0 + lck.l_whence = SEEK_SET; +#endif +#if F_WRLCK != 0 + lck.l_type = F_WRLCK; +#endif + if (fcntl( fd, F_SETLKW, &lck )) { + perror( ilname ); + err2: + close( fd ); + goto err1; + } + + if (!(fp = fopen( iuvname, "r" ))) { + perror( iuvname ); + goto err2; + } + fscanf( fp, "%d", &uidval ); + fclose( fp ); + if (!(fp = fopen( imuname, "r" ))) { + perror( imuname ); + goto err2; + } + fscanf( fp, "%d", &maxuid ); + fclose( fp ); + + if (!stat( iumname, &sb )) { + if (db_create( &db, 0, 0 )) { + fputs( "dbcreate failed\n", stderr ); + goto err2; + } + if (db->open( db, 0, iumname, 0, DB_HASH, 0, 0 )) { + fputs( "cannot open db\n", stderr ); + db->close( db, 0 ); + goto err2; + } + altmap++; + } else { + db = 0; + altmap--; + } + + msgs = 0; + rmsgs = 0; + nmsgs = 0; + for (i = 0; i < 2; i++) { + bl = nfsnprintf( buf, sizeof(buf), "%s/%s/", mboxdir, subdirs[i] ); + if (!(d = opendir( buf ))) { + perror( "opendir" ); + err4: + if (msgs) + free( msgs ); + if (db) + db->close( db, 0 ); + goto err2; + } + while ((e = readdir( d ))) { + if (*e->d_name == '.') + continue; + s = strchr( e->d_name, ':' ); + if (db) { + key.data = e->d_name; + key.size = s ? (size_t)(s - e->d_name) : strlen( e->d_name ); + if ((ret = db->get( db, 0, &key, &value, 0 ))) { + if (ret != DB_NOTFOUND) + db->err( db, ret, "Maildir error: db->get()" ); + continue; + } + uid = *(int *)value.data; + } else if ((p = strstr( e->d_name, ",U=" ))) + uid = atoi( p + 3 ); + else + continue; + if (nmsgs == rmsgs) { + rmsgs = rmsgs * 2 + 100; + msgs = nfrealloc( msgs, rmsgs * sizeof(msg_t) ); + } + msgs[nmsgs].uid = uid; + msgs[nmsgs++].flags = parse_info( s ); + } + closedir( d ); + } + + qsort( msgs, nmsgs, sizeof(msg_t), compare_uids ); + + if (!(fp = fopen( sname, "w" ))) { + perror( sname ); + goto err4; + } + if (box->max_messages) { + if (!nmsgs) + i = maxuid; + else { + i = nmsgs - box->max_messages; + if (i < 0) + i = 0; + i = msgs[i].uid - 1; + } + } else + i = 0; + fprintf( fp, "%d:%d %d:%d:%d\n", uidval, maxuid, uidval, i, maxuid ); + for (i = 0; i < nmsgs; i++) { + fprintf( fp, "%d %d ", msgs[i].uid, msgs[i].uid ); + for (u = 0; u < as(Flags); u++) + if (msgs[i].flags & (1 << u)) + fputc( Flags[u], fp ); + fputc( '\n', fp ); + } + fclose( fp ); + + if (db) { + key.data = (void *)"UIDVALIDITY"; + key.size = 11; + uv[0] = uidval; + uv[1] = maxuid; + value.data = uv; + value.size = sizeof(uv); + if ((ret = db->put( db, 0, &key, &value, 0 ))) { + db->err( db, ret, "Maildir error: db->put()" ); + goto err4; + } + db->close( db, 0 ); + rename( iumname, diumname ); + } else { + if (!(fp = fopen( uvname, "w" ))) { + perror( uvname ); + goto err4; + } + fprintf( fp, "%d\n%d\n", uidval, maxuid ); + fclose( fp ); + } + + unlink( iuvname ); + unlink( imuname ); + + close( fd ); + unlink( ilname ); + + if (msgs) + free( msgs ); + free( mboxdir ); + return; +} diff --git a/src/compat/isync.1 b/src/compat/isync.1 @@ -0,0 +1,318 @@ +.ig +\" isync - mbsync wrapper: IMAP4 to Maildir mailbox synchronizer +\" Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> +\" Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net> +\" +\" 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 "2004 Mar 27" +.. +.SH NAME +isync - synchronize IMAP4 and Maildir mailboxes +.. +.SH SYNOPSIS +\fBisync\fR [\fIoptions\fR ...] {\fImailbox\fR ...|\fI-a\fR|\fI-l\fR} +.. +.SH DESCRIPTION +\fBisync\fR is a command line application which synchronizes local +Maildir mailboxes with remote IMAP4 mailboxes, suitable for use in +IMAP-disconnected mode. Multiple copies of the remote IMAP4 mailboxes can +be maintained, and all flags are synchronized. +.br +\fBisync\fR is only a wrapper binary around \fBmbsync\fR to simplify upgrades. +It will automatically migrate the UID mapping from previous versions of +\fBisync\fR (even before 0.8) to the new format, and transparently call +\fBmbsync\fR. If you were using \fBisync\fR version 0.8 or 0.9.x you might +want to use \fBmdconvert\fR to convert the mailboxes to the more efficient +\fBnative\fR UID storage scheme after migrating them. +.. +.SH OPTIONS +.TP +\fB-c\fR, \fB--config\fR \fIfile\fR +Read configuration from \fIfile\fR. +By default, the configuration is read from ~/.isyncrc if it exists. +.TP +\fB-1\fR, \fB--one-to-one\fR +Instead of using the mailbox specifications in ~/.isyncrc, isync will pick up +all mailboxes from the local directory and remote folder and map them 1:1 +onto each other according to their names. +.TP +\fB-I\fR, \fB--inbox\fR \fImailbox\fR +Exception to the 1:1 mapping created by -1: the special IMAP mailbox \fIINBOX\fR +is mapped to the local \fImailbox\fR (relative to the maildir). +.TP +\fB-a\fR, \fB--all\fR +Synchronize all mailboxes (either specified in ~/.isyncrc or determined by the +1:1 mapping). +.TP +\fB-l\fR, \fB--list\fR +Don't synchronize anything, but list all mailboxes and exit. +.TP +\fB-L\fR, \fB--create-local\fR +Automatically create the local Maildir mailbox if it doesn't already +exist. +.TP +\fB-R\fR, \fB--create-remote\fR +Automatically create the remote IMAP mailbox if it doesn't already exist. +.TP +\fB-C\fR, \fB--create\fR +Automatically create any mailboxes if they don't already exist. +This is simply a combination of -L and -R. +.TP +\fB-d\fR, \fB--delete\fR +Causes \fBisync\fR to propagate message deletions. +By default, \fIdead\fR messages are \fBnot\fR deleted. +.TP +\fB-e\fR, \fB--expunge\fR +Causes \fBisync\fR to permanently remove all messages marked for deletion. +By default, \fIdeleted\fR messages are \fBnot\fR expunged. +.TP +\fB-f\fR, \fB--fast\fR +Only fetch new messages existing on the server into the local mailbox. +Message deletions and flag changes will not be propagated. +.TP +\fB-h\fR, \fB--help\fR +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 for imap, +993 for imaps) +.TP +\fB-q\fR, \fB--quiet\fR +Suppress informational messages. +If specified twice, suppress warning messages as well. +.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 [\fBimaps:\fR]\fIhost\fR +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 +\fB-M\fR, \fB--maildir\fR \fIdir\fR +Specifies the location for your local mailboxes. +.TP +\fB-F\fR, \fB--folder\fR \fIfolder\fR/ +Specifies the location for your remote mailboxes. +.TP +\fB-v\fR, \fB--version\fR +Displays \fBisync\fR version information. +.TP +\fB-V\fR, \fB--verbose\fR +Enables \fIverbose\fR mode, which displays the IMAP4 network traffic. +.TP +\fB-D\fR, \fB--debug\fR +Enable printing of \fIdebug\fR messages. +.TP +\fB-w\fR, \fB--write\fR +Don't run \fBmbsync\fR, but instead write a permanent config file for it. +The UID mappings of all configured mailboxes will be migrated. +Note that some command line options that would affect an actual sync operation +will be incorporated into the new config file as well. +The name of the new config file is determined by replacing the last occurrence +of "isync" with "mbsync", or appending ".mbsync" if "isync" was not found. +.TP +\fB-W\fR, \fB--writeto\fR \fIfile\fR +Like \fB-w\fR, but use the specified name for the new config file. +.. +.SH CONFIGURATION +\fBisync\fR by default reads \fI~/.isyncrc\fR to load configuration data. +Each non-empty line of the configuration file that does not start with a +hash mark 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 \fIMailbox\fR command, apply to this mailbox only. +.. +.TP +\fBHost\fR [\fBimaps:\fR]\fIname\fR +Defines the DNS name or IP address of the IMAP server. If the hostname is +prefixed with \fBimaps:\fR the connection is assumed to be a SSL connection +to port 993 (though you can change this by placing a \fBPort\fR command +\fBafter\fR the \fBHost\fR command). +Note that modern servers support SSL on the default port 143. +\fBisync\fR will always attempt to use SSL if available. +.. +.TP +\fBPort\fR \fIport\fR +Defines the TCP port number of the IMAP server (Default: 143 for imap, +993 for imaps) +.. +.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 \fIusername\fR on the IMAP server. +Note that this option is \fBNOT\fR required. +If no password is specified in the configuration file, \fBisync\fR +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. +.. +.TP +\fBCopyDeletedTo\fR \fImailbox\fR +Specifies the remote IMAP mailbox to copy deleted messages to prior to +expunging (Default: none). +.. +.TP +\fBDelete\fR \fIyes\fR|\fIno\fR +Specifies whether message deletions are propagated. (Default: no). +\fBNOTE:\fR The \fI-d\fR command line option overrides this setting when +set to \fIno\fR. +.. +.TP +\fBExpunge\fR \fIyes\fR|\fIno\fR +Specifies whether deleted messages are expunged. (Default: no). +\fBNOTE:\fR The \fI-e\fR command line option overrides this setting when +set to \fIno\fR. +.. +.TP +\fBMailDir\fR \fIdirectory\fR +Specifies the location of your local mailboxes if a relative path is +specified in a \fIMailbox\fR command (Default: \fI~\fR). +\fBNOTE:\fR This directive is allowed only in the \fIglobal\fR +section (see below). +.. +.TP +\fBFolder\fR \fIdirectory\fR/ +Specifies the location of your IMAP mailboxes +specified in \fIBox\fR commands (Default: \fI""\fR). +\fBNOTE:\fR You \fBmust\fR append the hierarchy delimiter (usually +a slash) to this specification. +\fBNOTE 2:\fR This directive is allowed only in the \fIglobal\fR +section (see below). +.. +.TP +\fBMaxMessages\fR \fIcount\fR +Sets the number of messages \fBisync\fR should keep in the local copy of a +mailbox. +This is useful for mailboxes where you keep a complete archive on the server, +but want to mirror only the last messages (for instance, for mailing lists). +The messages that were the first to arrive in the mailbox (independently of the +actual date of the message) will be deleted first. +Messages that are flagged (marked as important) and recent messages will not be +automatically deleted. +If \fIcount\fR is 0, the maximum number of messages is \fBunlimited\fR. +(Default: 0) +.. +.TP +\fBMaxSize\fR \fIbytes\fR +Messages larger than that many bytes will not be transferred over the wire. +This is useful for weeding out messages with large attachments. +If \fIbytes\fR is 0, the maximum file size is \fBunlimited\fR. +(Default: 0) +.. +.TP +\fBTunnel\fR \fIcommand\fR +Specify a command to run to establish a connection rather than opening a TCP +socket. This allows you to run an IMAP session over an SSH tunnel, for +example. +.TP +\fBUseNamespace\fR \fIyes\fR|\fIno\fR +Selects whether the server's first "personal" NAMESPACE should be prefixed to +mailbox names. Disabling this makes sense for some broken IMAP servers. +This option is meaningless if a \fIFolder\fR was specified. +(Default: \fIyes\fR) +.. +.TP +\fBRequireCRAM\fR \fIyes\fR|\fIno\fR +If set to \fIyes\fR, \fBisync\fR will abort the connection if no CRAM-MD5 +authentication is possible. (Default: \fIno\fR) +.. +.TP +\fBRequireSSL\fR \fIyes\fR|\fIno\fR +\fBisync\fR will abort the connection if a TLS/SSL session cannot be +established with the IMAP server. (Default: \fIyes\fR) +.. +.TP +\fBCertificateFile\fR \fIpath\fR +File containing X.509 CA certificates used to verify server identities. +.. +.TP +\fBUseSSLv2\fR \fIyes\fR|\fIno\fR +Should \fBisync\fR use SSLv2 for communication with the IMAP server over SSL? +(Default: \fIyes\fR if the imaps port is used, otherwise \fIno\fR) +.. +.TP +\fBUseSSLv3\fR \fIyes\fR|\fIno\fR +Should \fBisync\fR use SSLv3 for communication with the IMAP server over SSL? +(Default: \fIyes\fR if the imaps port is used, otherwise \fIno\fR) +.. +.TP +\fBUseTLSv1\fR \fIyes\fR|\fIno\fR +Should \fBisync\fR use TLSv1 for communication with the IMAP server over SSL? +(Default: \fIyes\fR) +.. +.TP +\fBOneToOne\fR +\fBisync\fR will ignore any \fIMailbox\fR specifications and instead pick up +all mailboxes from the local \fIMailDir\fR and remote \fIFolder\fR and map +them 1:1 onto each other according to their names. +\fBNOTE:\fR This directive is allowed only in the \fIglobal\fR +section (see below). +.. +.TP +\fBInbox\fR \fImailbox\fR +Exception to the OneToOne mapping: the special IMAP mailbox \fIINBOX\fR +is mapped to the local \fImailbox\fR (relative to the \fIMailDir\fR). +\fBNOTE:\fR This directive is only meaningful in the \fIglobal\fR +section (see below). +.. +.P +Configuration commands that appear prior to the first \fBMailbox\fR +command are considered to be \fIglobal\fR +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 \fBUser\fR command before +the first \fBMailbox\fR command, and then leave out the \fBUser\fR command +in the sections for each mailbox. +\fBisync\fR will then use the global value by default. +.. +.SH FILES +.TP +.B ~/.isyncrc +Default configuration file +.. +.SH BUGS +The configuration file takes precedence over command line options. +.br +Use -c /dev/null to work around. +.P +See the \fBINHERENT PROBLEMS\fR section in the \fBmbsync\fR man page, too. +.. +.SH SEE ALSO +mbsync(1), mdconvert(1), mutt(1), maildir(5) +.P +Up to date information on \fBisync\fR can be found at http://isync.sf.net/ +.. +.SH AUTHOR +Written by Michael R. Elkins <me@mutt.org>, +.br +maintained by Oswald Buddenhagen <ossi@users.sf.net>. diff --git a/src/compat/isync.h b/src/compat/isync.h @@ -0,0 +1,100 @@ +/* + * isync - mbsync wrapper: IMAP4 to maildir mailbox synchronizer + * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> + * Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net> + * + * 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 + */ + +#define _GNU_SOURCE + +#include <config.h> + +#include <sys/types.h> +#include <stdarg.h> + +#define as(ar) (sizeof(ar)/sizeof(ar[0])) + +#if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ > 4) +# define ATTR_UNUSED __attribute__((unused)) +# define ATTR_NORETURN __attribute__((noreturn)) +# define ATTR_PRINTFLIKE(fmt,var) __attribute__((format(printf,fmt,var))) +#else +# define ATTR_UNUSED +# define ATTR_NORETURN +# define ATTR_PRINTFLIKE(fmt,var) +#endif + +typedef struct config { + struct config *next; + + const char *server_name; + int servers; + char *host; + int port; + char *user; + char *pass; + char *tunnel; + unsigned int require_cram:1; + unsigned int require_ssl:1; + unsigned int use_imaps:1; + unsigned int use_sslv2:1; + unsigned int use_sslv3:1; + unsigned int use_tlsv1:1; + char *cert_file; + + const char *store_name; + int stores; + char *copy_deleted_to; + unsigned int use_namespace:1; + + const char *channel_name; + int channels; + const char *alias; + const char *box; + const char *path; /* path relative to .maildir, or absolute path */ + int max_size; + unsigned int max_messages; + unsigned int expunge:1; + unsigned int delete:1; +} config_t; + +extern int Quiet, Verbose, Debug; + +extern const char *Home; + +extern config_t global, *boxes; + +extern const char *maildir, *xmaildir, *folder, *inbox; +extern int o2o, altmap; + +/* config.c */ +void load_config( const char *, config_t *** ); +void write_config( int ); +char *expand_strdup( const char * ); +config_t *find_box( const char * ); + +/* convert.c */ +void convert( config_t * ); + +/* util.c */ +char *next_arg( char ** ); +void *nfmalloc( size_t sz ); +void *nfrealloc( void *mem, size_t sz ); +char *nfstrdup( const char *str ); +int nfvasprintf( char **str, const char *fmt, va_list va ); +int nfasprintf( char **str, const char *fmt, ... ); +int nfsnprintf( char *buf, int blen, const char *fmt, ... ); +void ATTR_NORETURN oob( void ); diff --git a/isyncrc.sample b/src/compat/isyncrc.sample diff --git a/src/compat/main.c b/src/compat/main.c @@ -0,0 +1,431 @@ +/* + * isync - mbsync wrapper: IMAP4 to maildir mailbox synchronizer + * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> + * Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net> + * + * 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 "isync.h" + +#include <sys/types.h> +#include <sys/stat.h> +#include <stdlib.h> +#include <unistd.h> +#include <fcntl.h> +#include <stdarg.h> +#include <limits.h> +#include <pwd.h> +#include <stdio.h> +#include <errno.h> +#include <string.h> +#include <ctype.h> +#include <dirent.h> + +#if HAVE_GETOPT_LONG +# define _GNU_SOURCE +# include <getopt.h> +struct option Opts[] = { + {"write", 0, NULL, 'w' }, + {"writeto", 0, NULL, 'W' }, + {"all", 0, NULL, 'a' }, + {"list", 0, NULL, 'l'}, + {"config", 1, NULL, 'c'}, + {"create", 0, NULL, 'C'}, + {"create-local", 0, NULL, 'L'}, + {"create-remote", 0, NULL, 'R'}, + {"delete", 0, NULL, 'd'}, + {"expunge", 0, NULL, 'e'}, + {"fast", 0, NULL, 'f'}, + {"help", 0, NULL, 'h'}, + {"remote", 1, NULL, 'r'}, + {"folder", 1, NULL, 'F'}, + {"maildir", 1, NULL, 'M'}, + {"one-to-one", 0, NULL, '1'}, + {"inbox", 1, NULL, 'I'}, + {"host", 1, NULL, 's'}, + {"port", 1, NULL, 'p'}, + {"debug", 0, NULL, 'D'}, + {"quiet", 0, NULL, 'q'}, + {"user", 1, NULL, 'u'}, + {"version", 0, NULL, 'v'}, + {"verbose", 0, NULL, 'V'}, + {0, 0, 0, 0} +}; +#endif + +static void +version( void ) +{ + puts( PACKAGE " " VERSION ); + exit( 0 ); +} + +static void +usage( int code ) +{ + fputs( +PACKAGE " " VERSION " - mbsync wrapper: IMAP4 to maildir synchronizer\n" +"Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org>\n" +"Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net>\n" +"usage:\n" +" " PACKAGE " [ flags ] mailbox [mailbox ...]\n" +" " PACKAGE " [ flags ] -a\n" +" " PACKAGE " [ flags ] -l\n" +" -a, --all synchronize all defined mailboxes\n" +" -l, --list list all defined mailboxes and exit\n" +" -L, --create-local create local maildir mailbox if nonexistent\n" +" -R, --create-remote create remote imap mailbox if nonexistent\n" +" -C, --create create both local and remote mailboxes if nonexistent\n" +" -d, --delete delete local msgs that don't exist on the server\n" +" -e, --expunge expunge deleted messages\n" +" -f, --fast only fetch new messages\n" +" -r, --remote BOX remote mailbox\n" +" -F, --folder DIR remote IMAP folder containing mailboxes\n" +" -M, --maildir DIR local directory containing mailboxes\n" +" -1, --one-to-one map every IMAP <folder>/box to <maildir>/box\n" +" -I, --inbox BOX map IMAP INBOX to <maildir>/BOX (exception to -1)\n" +" -s, --host HOST IMAP server address\n" +" -p, --port PORT server IMAP port\n" +" -u, --user USER IMAP user name\n" +" -c, --config CONFIG read an alternate config file (default: ~/.isyncrc)\n" +" -D, --debug print debugging messages\n" +" -V, --verbose verbose mode (display network traffic)\n" +" -q, --quiet don't display progress info\n" +" -v, --version display version\n" +" -h, --help display this help message\n\n" +"Note that this is a wrapper binary only; the \"real\" isync is named \"mbsync\".\n" +"Options to permanently transform your old isync configuration:\n" +" -w, --write write permanent mbsync configuration\n" +" -W, --writeto FILE write permanent mbsync configuration to FILE\n", + code ? stderr : stdout ); + exit( code ); +} + +static const char * +strrstr( const char *h, const char *n ) +{ + char *p = strstr( h, n ); + if (!p) + return 0; + do { + h = p; + p = strstr( h + 1, n ); + } while (p); + return h; +} + +static void +add_arg( char ***args, const char *arg ) +{ + int nu = 0; + if (*args) + for (; (*args)[nu]; nu++); + *args = nfrealloc( *args, sizeof(char *) * (nu + 2)); + (*args)[nu] = nfstrdup( arg ); + (*args)[nu + 1] = 0; +} + +#define OP_EXPUNGE (1<<0) +#define OP_DELETE (1<<1) +#define OP_FAST (1<<2) +#define OP_CREATE_REMOTE (1<<3) +#define OP_CREATE_LOCAL (1<<4) + +int Quiet, Verbose, Debug; +config_t global, *boxes; +const char *maildir, *xmaildir, *folder, *inbox; +int o2o, altmap; + +const char *Home; + +int +main( int argc, char **argv ) +{ + config_t *box, **stor; + char *config = 0, *outconfig = 0, **args; + int i, pl, fd, mod, all, list, ops, writeout; + struct stat st; + char path1[_POSIX_PATH_MAX], path2[_POSIX_PATH_MAX]; + + if (!(Home = getenv("HOME"))) { + fputs( "Fatal: $HOME not set\n", stderr ); + return 1; + } + + /* defaults */ + /* XXX the precedence is borked: + it's defaults < cmdline < file instead of defaults < file < cmdline */ + global.port = 143; + global.box = "INBOX"; + global.use_namespace = 1; + global.require_ssl = 1; + global.use_tlsv1 = 1; + folder = ""; + maildir = "~"; + xmaildir = Home; + +#define FLAGS "wW:alCLRc:defhp:qu:r:F:M:1I:s:vVD" + + mod = all = list = ops = writeout = Quiet = Verbose = Debug = 0; +#if HAVE_GETOPT_LONG + while ((i = getopt_long( argc, argv, FLAGS, Opts, NULL )) != -1) +#else + while ((i = getopt( argc, argv, FLAGS )) != -1) +#endif + { + switch (i) { + case 'W': + outconfig = optarg; + /* plopp */ + case 'w': + writeout = 1; + break; + case 'l': + list = 1; + /* plopp */ + case 'a': + all = 1; + break; + case '1': + o2o = 1; + mod = 1; + break; + case 'C': + ops |= OP_CREATE_REMOTE|OP_CREATE_LOCAL; + break; + case 'L': + ops |= OP_CREATE_LOCAL; + break; + case 'R': + ops |= OP_CREATE_REMOTE; + break; + case 'c': + config = optarg; + break; + case 'd': + ops |= OP_DELETE; + break; + case 'e': + ops |= OP_EXPUNGE; + break; + case 'f': + ops |= OP_FAST; + break; + case 'p': + global.port = atoi( optarg ); + mod = 1; + break; + case 'r': + global.box = optarg; + mod = 1; + break; + case 'F': + folder = optarg; + mod = 1; + break; + case 'M': + maildir = optarg; + mod = 1; + break; + case 'I': + inbox = optarg; + mod = 1; + break; + case 's': +#if HAVE_LIBSSL + if (!strncasecmp( "imaps:", optarg, 6 )) { + global.use_imaps = 1; + global.port = 993; + global.use_sslv2 = 1; + global.use_sslv3 = 1; + optarg += 6; + } +#endif + global.host = optarg; + mod = 1; + break; + case 'u': + global.user = optarg; + mod = 1; + break; + case 'D': + Debug = 1; + break; + case 'V': + Verbose = 1; + break; + case 'q': + Quiet++; + break; + case 'v': + version(); + case 'h': + usage( 0 ); + default: + usage( 1 ); + } + } + + if (config) { + if (*config != '/') { + if (!getcwd( path1, sizeof(path1) )) { + fprintf( stderr, "Can't obtain working directory\n" ); + return 1; + } + pl = strlen( path1 ); + nfsnprintf( path1 + pl, sizeof(path1) - pl, "/%s", config ); + config = path1; + } + } else { + nfsnprintf( path1, sizeof(path1), "%s/.isyncrc", Home ); + config = path1; + } + stor = &boxes; + load_config( config, &stor ); + + if (!all && !o2o) + for (i = optind; argv[i]; i++) + if (!(box = find_box( argv[i] ))) { + box = nfmalloc( sizeof(config_t) ); + memcpy( box, &global, sizeof(config_t) ); + box->path = argv[i]; + *stor = box; + stor = &box->next; + mod = 1; + } + + if (writeout) { + all = 1; + if (mod) + fprintf( stderr, + "Warning: command line switches that influence the resulting config file\n" + "have been supplied.\n" ); + } else { + if (!argv[optind] && !all) { + fprintf( stderr, "No mailbox specified. Try isync -h\n" ); + return 1; + } + } + + if (all) { + if (o2o) { + DIR * dir; + struct dirent *de; + + if (!(dir = opendir( xmaildir ))) { + fprintf( stderr, "%s: %s\n", xmaildir, strerror(errno) ); + return 1; + } + while ((de = readdir( dir ))) { + if (*de->d_name == '.') + continue; + nfsnprintf( path1, sizeof(path1), "%s/%s/cur", xmaildir, de->d_name ); + if (stat( path1, &st ) || !S_ISDIR( st.st_mode )) + continue; + global.path = de->d_name; + global.box = (inbox && !strcmp( inbox, global.path )) ? + "INBOX" : global.path; + convert( &global ); + } + closedir( dir ); + } else + for (box = boxes; box; box = box->next) + convert( box ); + } else { + for (i = optind; argv[i]; i++) + if (o2o) { + global.path = argv[i]; + global.box = + (inbox && !strcmp( global.path, inbox )) ? + "INBOX" : global.path; + convert( &global ); + } else + convert( find_box( argv[i] ) ); + } + + if (writeout) { + if (!outconfig) { + const char *p = strrchr( config, '/' ); + if (!p) + p = config; + p = strrstr( p, "isync" ); + if (!p) + nfsnprintf( path2, sizeof(path2), "%s.mbsync", config ); + else + nfsnprintf( path2, sizeof(path2), "%.*smb%s", p - config, config, p + 1 ); + outconfig = path2; + } + if ((fd = creat( outconfig, 0666 )) < 0) { + fprintf( stderr, "Error: cannot write new config %s: %s\n", outconfig, strerror(errno) ); + return 1; + } + } else { + strcpy( path2, "/tmp/mbsyncrcXXXXXX" ); + if ((fd = mkstemp( path2 )) < 0) { + fprintf( stderr, "Can't create temp file\n" ); + return 1; + } + } + write_config( fd ); + + if (writeout) + return 0; + args = 0; + add_arg( &args, "mbsync" ); + if (Verbose) + add_arg( &args, "-V" ); + if (Debug) + add_arg( &args, "-D" ); + for (; Quiet; Quiet--) + add_arg( &args, "-q" ); + add_arg( &args, "-cT" ); + add_arg( &args, path2 ); + if (ops & OP_FAST) + add_arg( &args, "-Ln" ); + else if (!(ops & OP_DELETE)) + add_arg( &args, "-nNf" ); + if (ops & OP_CREATE_REMOTE) + add_arg( &args, "-Cm" ); + if (ops & OP_CREATE_LOCAL) + add_arg( &args, "-Cs" ); + if (ops & OP_EXPUNGE) + add_arg( &args, "-X" ); + if (list) + add_arg( &args, "-l" ); + if (o2o) { + if (all) + add_arg( &args, "o2o" ); + else { + char buf[1024]; + strcpy( buf, "o2o:" ); + strcat( buf, argv[optind] ); + while (argv[++optind]) { + strcat( buf, "," ); + strcat( buf, argv[optind] ); + } + add_arg( &args, buf ); + } + } else { + if (all) + add_arg( &args, "-a" ); + else + for (; argv[optind]; optind++) + add_arg( &args, find_box( argv[optind] )->channel_name ); + } + execvp( args[0], args ); + perror( args[0] ); + return 1; +} diff --git a/src/compat/util.c b/src/compat/util.c @@ -0,0 +1,156 @@ +/* + * isync - mbsync wrapper: IMAP4 to maildir mailbox synchronizer + * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> + * Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net> + * + * 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 "isync.h" + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <pwd.h> +#include <ctype.h> + +char * +next_arg( char **s ) +{ + char *ret; + + if (!s || !*s) + return 0; + while (isspace( (unsigned char) **s )) + (*s)++; + if (!**s) { + *s = 0; + return 0; + } + if (**s == '"') { + ++*s; + ret = *s; + *s = strchr( *s, '"' ); + } else { + ret = *s; + while (**s && !isspace( (unsigned char) **s )) + (*s)++; + } + if (*s) { + if (**s) + *(*s)++ = 0; + if (!**s) + *s = 0; + } + return ret; +} + +#ifndef HAVE_VASPRINTF +static int +vasprintf( char **strp, const char *fmt, va_list ap ) +{ + int len; + char tmp[1024]; + + if ((len = vsnprintf( tmp, sizeof(tmp), fmt, ap) ) < 0) + *strp = 0; + else if ((*strp = malloc( len + 1 ))) { + if (len >= sizeof(tmp)) + vsprintf( *strp, fmt, ap ); + else + memcpy( *strp, tmp, len + 1 ); + } + return len; +} +#endif + +void +oob( void ) +{ + fputs( "Fatal: buffer too small. Please report a bug.\n", stderr ); + abort(); +} + +int +nfsnprintf( char *buf, int blen, const char *fmt, ... ) +{ + int ret; + va_list va; + + va_start( va, fmt ); + if (blen <= 0 || (unsigned)(ret = vsnprintf( buf, blen, fmt, va )) >= (unsigned)blen) + oob(); + va_end( va ); + return ret; +} + +static void ATTR_NORETURN +oom( void ) +{ + fputs( "Fatal: Out of memory\n", stderr ); + abort(); +} + +void * +nfmalloc( size_t sz ) +{ + void *ret; + + if (!(ret = malloc( sz ))) + oom(); + return ret; +} + +void * +nfrealloc( void *mem, size_t sz ) +{ + char *ret; + + if (!(ret = realloc( mem, sz )) && sz) + oom(); + return ret; +} + +char * +nfstrdup( const char *str ) +{ + char *ret; + + if (!(ret = strdup( str ))) + oom(); + return ret; +} + +int +nfvasprintf( char **str, const char *fmt, va_list va ) +{ + int ret = vasprintf( str, fmt, va ); + if (!*str) + oom(); + return ret; +} + +int +nfasprintf( char **str, const char *fmt, ... ) +{ + int ret; + va_list va; + + va_start( va, fmt ); + ret = nfvasprintf( str, fmt, va ); + va_end( va ); + return ret; +} diff --git a/src/config.c b/src/config.c @@ -1,8 +1,7 @@ -/* $Id$ - * - * isync - IMAP4 to maildir mailbox synchronizer +/* + * mbsync - mailbox synchronizer * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> - * Copyright (C) 2002-2003 Oswald Buddenhagen <ossi@users.sf.net> + * Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net> * * 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 @@ -18,7 +17,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * - * As a special exception, isync may be linked with the OpenSSL library, + * As a special exception, mbsync may be linked with the OpenSSL library, * despite that library's more restrictive license. */ @@ -33,254 +32,411 @@ #include <stdlib.h> #include <stdio.h> -config_t *boxes = 0; +store_conf_t *stores; +channel_conf_t *channels; +group_conf_t *groups; +int global_mops, global_sops; +char *global_sync_state; -/* set defaults from the global configuration section */ -static void -config_defaults (config_t * conf) +int +parse_bool( conffile_t *cfile ) { - memcpy (conf, &global, sizeof (config_t)); + if (!strcasecmp( cfile->val, "yes" ) || + !strcasecmp( cfile->val, "true" ) || + !strcasecmp( cfile->val, "on" ) || + !strcmp( cfile->val, "1" )) + return 1; + if (strcasecmp( cfile->val, "no" ) && + strcasecmp( cfile->val, "false" ) && + strcasecmp( cfile->val, "off" ) && + strcmp( cfile->val, "0" )) + fprintf( stderr, "%s:%d: invalid boolean value '%s'\n", + cfile->file, cfile->line, cfile->val ); + return 0; } -static char * -my_strndup (const char *s, size_t nchars) +int +parse_int( conffile_t *cfile ) { - char *r = malloc (sizeof (char) * (nchars + 1)); - if (r) - { - memcpy (r, s, nchars); - r[nchars] = 0; - } - return r; + char *p; + int ret; + + ret = strtol( cfile->val, &p, 10 ); + if (*p) { + fprintf( stderr, "%s:%d: invalid integer value '%s'\n", + cfile->file, cfile->line, cfile->val ); + return 0; + } + return ret; } -char * -expand_strdup (const char *s) +int +parse_size( conffile_t *cfile ) { - char path[_POSIX_PATH_MAX]; - struct passwd *pw; - const char *p; + char *p; + int ret; - if (*s == '~') - { - s++; - if (*s == '/') - { - /* current user */ - pw = getpwuid (getuid ()); - p = s + 1; - } - else - { - char *user; - - p = strchr (s, '/'); - if (p) - { - user = my_strndup (s, (int)(p - s)); + ret = strtol (cfile->val, &p, 10); + if (*p == 'k' || *p == 'K') + ret *= 1024, p++; + else if (*p == 'm' || *p == 'M') + ret *= 1024 * 1024, p++; + if (*p == 'b' || *p == 'B') p++; - } - else - user = strdup (s); - pw = getpwnam (user); - free (user); + if (*p) { + fprintf (stderr, "%s:%d: invalid size '%s'\n", + cfile->file, cfile->line, cfile->val); + return 0; } - if (!pw) - return 0; - snprintf (path, sizeof (path), "%s/%s", pw->pw_dir, p ? p : ""); - s = path; - } - else if (*s != '/') - { - snprintf (path, sizeof (path), "%s/%s", - global.maildir ? global.maildir : "", s); - s = path; - } - return strdup (s); + return ret; } -static int -is_true (const char *val) +static int +getopt_helper( conffile_t *cfile, int *cops, int *mops, int *sops, char **sync_state ) { - return - !strcasecmp (val, "yes") || - !strcasecmp (val, "true") || - !strcasecmp (val, "on") || - !strcmp (val, "1"); + char *arg; + + if (!strcasecmp( "Sync", cfile->cmd )) { + arg = cfile->val; + do + if (!strcasecmp( "Push", arg )) + *cops |= XOP_PUSH; + else if (!strcasecmp( "Pull", arg )) + *cops |= XOP_PULL; + else if (!strcasecmp( "ReNew", arg )) + *cops |= OP_RENEW; + else if (!strcasecmp( "New", arg )) + *cops |= OP_NEW; + else if (!strcasecmp( "Delete", arg )) + *cops |= OP_DELETE; + else if (!strcasecmp( "Flags", arg )) + *cops |= OP_FLAGS; + else if (!strcasecmp( "PullReNew", arg )) + *sops |= OP_RENEW; + else if (!strcasecmp( "PullNew", arg )) + *sops |= OP_NEW; + else if (!strcasecmp( "PullDelete", arg )) + *sops |= OP_DELETE; + else if (!strcasecmp( "PullFlags", arg )) + *sops |= OP_FLAGS; + else if (!strcasecmp( "PushReNew", arg )) + *mops |= OP_RENEW; + else if (!strcasecmp( "PushNew", arg )) + *mops |= OP_NEW; + else if (!strcasecmp( "PushDelete", arg )) + *mops |= OP_DELETE; + else if (!strcasecmp( "PushFlags", arg )) + *mops |= OP_FLAGS; + else if (!strcasecmp( "All", arg ) || !strcasecmp( "Full", arg )) + *cops |= XOP_PULL|XOP_PUSH; + else if (strcasecmp( "None", arg ) && strcasecmp( "Noop", arg )) + fprintf( stderr, "%s:%d: invalid Sync arg '%s'\n", + cfile->file, cfile->line, arg ); + while ((arg = next_arg( &cfile->rest ))); + *mops |= XOP_HAVE_TYPE; + } else if (!strcasecmp( "Expunge", cfile->cmd )) { + arg = cfile->val; + do + if (!strcasecmp( "Both", arg )) + *cops |= OP_EXPUNGE; + else if (!strcasecmp( "Master", arg )) + *mops |= OP_EXPUNGE; + else if (!strcasecmp( "Slave", arg )) + *sops |= OP_EXPUNGE; + else if (strcasecmp( "None", arg )) + fprintf( stderr, "%s:%d: invalid Expunge arg '%s'\n", + cfile->file, cfile->line, arg ); + while ((arg = next_arg( &cfile->rest ))); + *mops |= XOP_HAVE_EXPUNGE; + } else if (!strcasecmp( "Create", cfile->cmd )) { + arg = cfile->val; + do + if (!strcasecmp( "Both", arg )) + *cops |= OP_CREATE; + else if (!strcasecmp( "Master", arg )) + *mops |= OP_CREATE; + else if (!strcasecmp( "Slave", arg )) + *sops |= OP_CREATE; + else if (strcasecmp( "None", arg )) + fprintf( stderr, "%s:%d: invalid Create arg '%s'\n", + cfile->file, cfile->line, arg ); + while ((arg = next_arg( &cfile->rest ))); + *mops |= XOP_HAVE_CREATE; + } else if (!strcasecmp( "SyncState", cfile->cmd )) + *sync_state = expand_strdup( cfile->val ); + else + return 0; + return 1; } -void -load_config (const char *where, int *o2o) +int +getcline( conffile_t *cfile ) { - char path[_POSIX_PATH_MAX]; - char buf[1024]; - struct passwd *pw; - config_t **stor = &boxes, *cfg; - int line = 0; - FILE *fp; - char *p, *cmd, *val; + char *p; - if (!where) - { - pw = getpwuid (getuid ()); - snprintf (path, sizeof (path), "%s/.isyncrc", pw->pw_dir); - where = path; - } + while (fgets( cfile->buf, cfile->bufl, cfile->fp )) { + cfile->line++; + p = cfile->buf; + if (!(cfile->cmd = next_arg( &p ))) + return 1; + if (*cfile->cmd == '#') + continue; + if (!(cfile->val = next_arg( &p ))) { + fprintf( stderr, "%s:%d: parameter missing\n", + cfile->file, cfile->line ); + continue; + } + cfile->rest = p; + return 1; + } + return 0; +} - info ("Reading configuration file %s\n", where); +/* XXX - this does not detect None conflicts ... */ +int +merge_ops( int cops, int *mops, int *sops ) +{ + int aops; - fp = fopen (where, "r"); - if (!fp) - { - if (errno != ENOENT) - perror ("fopen"); - return; - } - buf[sizeof buf - 1] = 0; - cfg = &global; - while ((fgets (buf, sizeof (buf) - 1, fp))) - { - p = buf; - cmd = next_arg (&p); - val = next_arg (&p); - line++; - if (!cmd || *cmd == '#') - continue; - if (!val) { - fprintf (stderr, "%s:%d: parameter missing\n", path, line); - continue; - } - if (!strcasecmp ("mailbox", cmd)) - { - if (*o2o) - break; - cfg = *stor = malloc (sizeof (config_t)); - stor = &cfg->next; - config_defaults (cfg); - /* not expanded at this point */ - cfg->path = strdup (val); - } - else if (!strcasecmp ("OneToOne", cmd)) - { - if (boxes) { - forbid: - fprintf (stderr, - "%s:%d: keyword '%s' allowed only in global section\n", - path, line, cmd); - continue; - } - *o2o = is_true (val); + aops = *mops | *sops; + if (*mops & XOP_HAVE_TYPE) { + if (aops & OP_MASK_TYPE) { + if (aops & cops & OP_MASK_TYPE) { + cfl: + fprintf( stderr, "Conflicting Sync args specified.\n" ); + return 1; + } + *mops |= cops & OP_MASK_TYPE; + *sops |= cops & OP_MASK_TYPE; + if (cops & XOP_PULL) { + if (*sops & OP_MASK_TYPE) + goto cfl; + *sops |= OP_MASK_TYPE; + } + if (cops & XOP_PUSH) { + if (*mops & OP_MASK_TYPE) + goto cfl; + *mops |= OP_MASK_TYPE; + } + } else if (cops & (OP_MASK_TYPE|XOP_MASK_DIR)) { + if (!(cops & OP_MASK_TYPE)) + cops |= OP_MASK_TYPE; + else if (!(cops & XOP_MASK_DIR)) + cops |= XOP_PULL|XOP_PUSH; + if (cops & XOP_PULL) + *sops |= cops & OP_MASK_TYPE; + if (cops & XOP_PUSH) + *mops |= cops & OP_MASK_TYPE; + } } - else if (!strcasecmp ("maildir", cmd)) - { - if (boxes) - goto forbid; - /* this only affects the global setting */ - global.maildir = expand_strdup (val); + if (*mops & XOP_HAVE_EXPUNGE) { + if (aops & cops & OP_EXPUNGE) { + fprintf( stderr, "Conflicting Expunge args specified.\n" ); + return 1; + } + *mops |= cops & OP_EXPUNGE; + *sops |= cops & OP_EXPUNGE; } - else if (!strcasecmp ("folder", cmd)) - { - if (boxes) - goto forbid; - /* this only affects the global setting */ - global.folder = strdup (val); + if (*mops & XOP_HAVE_CREATE) { + if (aops & cops & OP_CREATE) { + fprintf( stderr, "Conflicting Create args specified.\n" ); + return 1; + } + *mops |= cops & OP_CREATE; + *sops |= cops & OP_CREATE; } - else if (!strcasecmp ("inbox", cmd)) - { - if (boxes) - goto forbid; - /* this only affects the global setting */ - global.inbox = strdup (val); - } - else if (!strcasecmp ("host", cmd)) - { -#if HAVE_LIBSSL - if (!strncasecmp ("imaps:", val, 6)) - { - val += 6; - cfg->use_imaps = 1; - cfg->port = 993; - cfg->use_sslv2 = 1; - cfg->use_sslv3 = 1; - } -#endif - cfg->host = strdup (val); - } - else if (!strcasecmp ("user", cmd)) - cfg->user = strdup (val); - else if (!strcasecmp ("pass", cmd)) - cfg->pass = strdup (val); - else if (!strcasecmp ("port", cmd)) - cfg->port = atoi (val); - else if (!strcasecmp ("box", cmd)) - cfg->box = strdup (val); - else if (!strcasecmp ("alias", cmd)) - { - if (!boxes) { - fprintf (stderr, - "%s:%d: keyword 'alias' allowed only in mailbox specification\n", - path, line); - continue; - } - cfg->alias = strdup (val); - } - else if (!strcasecmp ("maxsize", cmd)) - cfg->max_size = atol (val); - else if (!strcasecmp ("MaxMessages", cmd)) - cfg->max_messages = atol (val); - else if (!strcasecmp ("UseNamespace", cmd)) - cfg->use_namespace = is_true (val); - else if (!strcasecmp ("CopyDeletedTo", cmd)) - cfg->copy_deleted_to = strdup (val); - else if (!strcasecmp ("Tunnel", cmd)) - cfg->tunnel = strdup (val); - else if (!strcasecmp ("Expunge", cmd)) - cfg->expunge = is_true (val); - else if (!strcasecmp ("Delete", cmd)) - cfg->delete = is_true (val); -#if HAVE_LIBSSL - else if (!strcasecmp ("CertificateFile", cmd)) - cfg->cert_file = expand_strdup (val); - else if (!strcasecmp ("RequireSSL", cmd)) - cfg->require_ssl = is_true (val); - else if (!strcasecmp ("UseSSLv2", cmd)) - cfg->use_sslv2 = is_true (val); - else if (!strcasecmp ("UseSSLv3", cmd)) - cfg->use_sslv3 = is_true (val); - else if (!strcasecmp ("UseTLSv1", cmd)) - cfg->use_tlsv1 = is_true (val); - else if (!strcasecmp ("RequireCRAM", cmd)) - cfg->require_cram = is_true (val); -#endif - else if (buf[0]) - fprintf (stderr, "%s:%d: unknown keyword '%s'\n", path, line, cmd); - } - fclose (fp); + return 0; } -config_t * -find_box (const char *s) +int +load_config( const char *where, int pseudo ) { - config_t *p = boxes; + conffile_t cfile; + store_conf_t *store, **storeapp = &stores, **sptarg; + channel_conf_t *channel, **channelapp = &channels; + group_conf_t *group, **groupapp = &groups; + string_list_t *chanlist, **chanlistapp; + char *arg, *p, **ntarg; + int err, len, cops, gcops, max_size; + char path[_POSIX_PATH_MAX]; + char buf[1024]; - for (; p; p = p->next) - { - if (!strcmp (s, p->path) || (p->alias && !strcmp (s, p->alias))) - return p; - else - { - /* check to see if the full pathname was specified on the - * command line. - */ - char *t = expand_strdup (p->path); + if (!where) { + nfsnprintf( path, sizeof(path), "%s/." EXE "rc", Home ); + cfile.file = path; + } else + cfile.file = where; + + if (!pseudo) + info( "Reading configuration file %s\n", cfile.file ); + + if (!(cfile.fp = fopen( cfile.file, "r" ))) { + perror( "Cannot open config file" ); + return 1; + } + buf[sizeof(buf) - 1] = 0; + cfile.buf = buf; + cfile.bufl = sizeof(buf) - 1; + cfile.line = 0; + + gcops = err = 0; + reloop: + while (getcline( &cfile )) { + if (!cfile.cmd) + continue; + if (imap_driver.parse_store( &cfile, &store, &err ) || + maildir_driver.parse_store( &cfile, &store, &err )) + { + if (store) { + if (!store->path) + store->path = ""; + *storeapp = store; + storeapp = &store->next; + *storeapp = 0; + } + } + else if (!strcasecmp( "Channel", cfile.cmd )) + { + channel = nfcalloc( sizeof(*channel) ); + channel->name = nfstrdup( cfile.val ); + cops = 0; + max_size = -1; + while (getcline( &cfile ) && cfile.cmd) { + if (!strcasecmp( "MaxSize", cfile.cmd )) + max_size = parse_size( &cfile ); + else if (!strcasecmp( "MaxMessages", cfile.cmd )) + channel->max_messages = parse_int( &cfile ); + else if (!strcasecmp( "Pattern", cfile.cmd ) || + !strcasecmp( "Patterns", cfile.cmd )) + { + arg = cfile.val; + do + add_string_list( &channel->patterns, arg ); + while ((arg = next_arg( &cfile.rest ))); + } + else if (!strcasecmp( "Master", cfile.cmd )) { + sptarg = &channel->master; + ntarg = &channel->master_name; + goto linkst; + } else if (!strcasecmp( "Slave", cfile.cmd )) { + sptarg = &channel->slave; + ntarg = &channel->slave_name; + linkst: + if (*cfile.val != ':' || !(p = strchr( cfile.val + 1, ':' ))) { + fprintf( stderr, "%s:%d: malformed mailbox spec\n", + cfile.file, cfile.line ); + err = 1; + continue; + } + *p = 0; + for (store = stores; store; store = store->next) + if (!strcmp( store->name, cfile.val + 1 )) { + memset( sptarg, 0, sizeof(*sptarg) ); + *sptarg = store; + goto stpcom; + } + fprintf( stderr, "%s:%d: unknown store '%s'\n", + cfile.file, cfile.line, cfile.val + 1 ); + err = 1; + continue; + stpcom: + if (*++p) + *ntarg = nfstrdup( p ); + } else if (!getopt_helper( &cfile, &cops, &channel->mops, &channel->sops, &channel->sync_state )) { + fprintf( stderr, "%s:%d: unknown keyword '%s'\n", + cfile.file, cfile.line, cfile.cmd ); + err = 1; + } + } + if (!channel->master) { + fprintf( stderr, "channel '%s' refers to no master store\n", channel->name ); + err = 1; + } else if (!channel->slave) { + fprintf( stderr, "channel '%s' refers to no slave store\n", channel->name ); + err = 1; + } else if (merge_ops( cops, &channel->mops, &channel->sops )) + err = 1; + else { + if (max_size >= 0) + channel->master->max_size = channel->slave->max_size = max_size; + *channelapp = channel; + channelapp = &channel->next; + } + } + else if (!strcasecmp( "Group", cfile.cmd )) + { + group = nfmalloc( sizeof(*group) ); + group->name = nfstrdup( cfile.val ); + *groupapp = group; + groupapp = &group->next; + *groupapp = 0; + chanlistapp = &group->channels; + *chanlistapp = 0; + p = cfile.rest; + while ((arg = next_arg( &p ))) { + addone: + len = strlen( arg ); + chanlist = nfmalloc( sizeof(*chanlist) + len ); + memcpy( chanlist->string, arg, len + 1 ); + *chanlistapp = chanlist; + chanlistapp = &chanlist->next; + *chanlistapp = 0; + } + while (getcline( &cfile )) { + if (!cfile.cmd) + goto reloop; + if (!strcasecmp( "Channel", cfile.cmd ) || + !strcasecmp( "Channels", cfile.cmd )) + { + p = cfile.rest; + arg = cfile.val; + goto addone; + } + else + { + fprintf( stderr, "%s:%d: unknown keyword '%s'\n", + cfile.file, cfile.line, cfile.cmd ); + err = 1; + } + } + break; + } + else if (!getopt_helper( &cfile, &gcops, &global_mops, &global_sops, &global_sync_state )) + { + fprintf( stderr, "%s:%d: unknown section keyword '%s'\n", + cfile.file, cfile.line, cfile.cmd ); + err = 1; + while (getcline( &cfile )) + if (!cfile.cmd) + goto reloop; + break; + } + } + fclose (cfile.fp); + err |= merge_ops( gcops, &global_mops, &global_sops ); + if (!global_sync_state) + global_sync_state = expand_strdup( "~/." EXE "/" ); + if (!err && pseudo) + unlink( where ); + return err; +} - if (!strcmp (s, t)) - { - free (t); - return p; - } - free (t); +void +parse_generic_store( store_conf_t *store, conffile_t *cfg, int *err ) +{ + if (!strcasecmp( "Trash", cfg->cmd )) + store->trash = nfstrdup( cfg->val ); + else if (!strcasecmp( "TrashRemoteNew", cfg->cmd )) + store->trash_remote_new = parse_bool( cfg ); + else if (!strcasecmp( "TrashNewOnly", cfg->cmd )) + store->trash_only_new = parse_bool( cfg ); + else if (!strcasecmp( "MaxSize", cfg->cmd )) + store->max_size = parse_size( cfg ); + else if (!strcasecmp( "MapInbox", cfg->cmd )) + store->map_inbox = nfstrdup( cfg->val ); + else { + fprintf( stderr, "%s:%d: unknown keyword '%s'\n", + cfg->file, cfg->line, cfg->cmd ); + *err = 1; } - } - return 0; } diff --git a/src/cram.c b/src/cram.c @@ -1,89 +0,0 @@ -/* $Id$ - * - * isync - IMAP4 to maildir mailbox synchronizer - * Copyright (C) 2000-2002 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 - * - * As a special exception, isync may be linked with the OpenSSL library, - * despite that library's more restrictive license. - */ - -#include <config.h> - -#if HAVE_LIBSSL - -#include "isync.h" - -#include <assert.h> -#include <string.h> -#include <openssl/hmac.h> - -#define ENCODED_SIZE(n) (4*((n+2)/3)) - -static char -hexchar (unsigned int b) -{ - if (b < 10) - return '0' + b; - return 'a' + (b - 10); -} - -char * -cram (const char *challenge, const char *user, const char *pass) -{ - HMAC_CTX hmac; - char hash[16]; - char hex[33]; - int i; - unsigned int hashlen = sizeof (hash); - char buf[256]; - int len = strlen (challenge); - char *response = calloc (1, 1 + len); - char *final; - - /* response will always be smaller than challenge because we are - * decoding. - */ - len = EVP_DecodeBlock ((unsigned char *) response, (unsigned char *) challenge, strlen (challenge)); - - HMAC_Init (&hmac, (unsigned char *) pass, strlen (pass), EVP_md5 ()); - HMAC_Update (&hmac, (unsigned char *) response, strlen(response)); - HMAC_Final (&hmac, (unsigned char *) hash, &hashlen); - - assert (hashlen == sizeof (hash)); - - free (response); - - hex[32] = 0; - for (i = 0; i < 16; i++) - { - hex[2 * i] = hexchar ((hash[i] >> 4) & 0xf); - hex[2 * i + 1] = hexchar (hash[i] & 0xf); - } - - snprintf (buf, sizeof (buf), "%s %s", user, hex); - - len = strlen (buf); - len = ENCODED_SIZE (len) + 1; - final = malloc (len); - final[len - 1] = 0; - - assert (EVP_EncodeBlock ((unsigned char *) final, (unsigned char *) buf, strlen (buf)) == len - 1); - - return final; -} - -#endif diff --git a/src/dotlock.c b/src/dotlock.c @@ -1,102 +0,0 @@ -/* $Id$ - * - * isync - IMAP4 to maildir mailbox synchronizer - * Copyright (C) 2002 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 - * - * As a special exception, isync may be linked with the OpenSSL library, - * despite that library's more restrictive license. - */ - -/* - * this file contains routines to establish a mutex using a `dotlock' file - */ - -#include "dotlock.h" - -#include <unistd.h> -#include <string.h> -#include <fcntl.h> -#include <sys/stat.h> -#if TESTING -#include <stdio.h> -#endif - -int dotlock_lock (const char *path, int *fd) -{ - struct flock lck; - - *fd = open (path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR); - if (*fd == -1) - return -1; - memset (&lck, 0, sizeof(lck)); -#if SEEK_SET != 0 - lck.l_whence = SEEK_SET; -#endif -#if F_WRLCK != 0 - lck.l_type = F_WRLCK; -#endif - if (fcntl (*fd, F_SETLK, &lck)) - { - close (*fd); - *fd = -1; - return -1; - } - return 0; -} - -int dotlock_unlock (int *fd) -{ - int r = 0; - struct flock lck; - - if (*fd != -1) - { - memset (&lck, 0, sizeof(lck)); -#if SEEK_SET != 0 - lck.l_whence = SEEK_SET; -#endif -#if F_UNLCK != 0 - lck.l_type = F_UNLCK; -#endif - if (fcntl (*fd, F_SETLK, &lck)) - r = -1; - close (*fd); - *fd = -1; - } - return r; -} - -#if TESTING -int main (void) -{ - int fd; - - if (dotlock_lock ("./lock", &fd)) - { - perror ("dotlock_lock"); - goto done; - } - puts ("sleeping for 5 seconds"); - sleep(5); - if (dotlock_unlock (&fd)) - { - perror ("dotlock_unlock"); - } -done: - exit (0); -} -#endif diff --git a/src/dotlock.h b/src/dotlock.h @@ -1,25 +0,0 @@ -/* $Id$ - * - * isync - IMAP4 to maildir mailbox synchronizer - * Copyright (C) 2002 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 - * - * As a special exception, isync may be linked with the OpenSSL library, - * despite that library's more restrictive license. - */ - -int dotlock_lock (const char *, int *); -int dotlock_unlock (int *); diff --git a/src/drv_imap.c b/src/drv_imap.c @@ -0,0 +1,1817 @@ +/* + * mbsync - mailbox synchronizer + * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> + * Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net> + * Copyright (C) 2004 Theodore Y. Ts'o <tytso@mit.edu> + * + * 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 + * + * As a special exception, mbsync may be linked with the OpenSSL library, + * despite that library's more restrictive license. + */ + +#include "isync.h" + +#include <assert.h> +#include <unistd.h> +#include <sys/mman.h> +#include <sys/time.h> +#include <stdlib.h> +#include <stdio.h> +#include <stdarg.h> +#include <errno.h> +#include <string.h> +#include <ctype.h> +#include <sys/socket.h> +#include <sys/ioctl.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <arpa/inet.h> +#include <netdb.h> +#if HAVE_LIBSSL +# include <openssl/ssl.h> +# include <openssl/err.h> +# include <openssl/hmac.h> +#endif + +typedef struct imap_server_conf { + struct imap_server_conf *next; + char *name; + char *tunnel; + char *host; + int port; + char *user; + char *pass; +#if HAVE_LIBSSL + char *cert_file; + unsigned use_imaps:1; + unsigned require_ssl:1; + unsigned use_sslv2:1; + unsigned use_sslv3:1; + unsigned use_tlsv1:1; + unsigned require_cram:1; +#endif +} imap_server_conf_t; + +typedef struct imap_store_conf { + store_conf_t gen; + imap_server_conf_t *server; + unsigned use_namespace:1; +} imap_store_conf_t; + +typedef struct imap_message { + message_t gen; +/* int seq; will be needed when expunges are tracked */ +} imap_message_t; + +#define NIL (void*)0x1 +#define LIST (void*)0x2 + +typedef struct _list { + struct _list *next, *child; + char *val; + int len; +} list_t; + +typedef struct { + int fd; +#if HAVE_LIBSSL + SSL *ssl; + unsigned int use_ssl:1; +#endif +} Socket_t; + +typedef struct { + Socket_t sock; + int bytes; + int offset; + char buf[1024]; +} buffer_t; + +struct imap_cmd; +#define max_in_progress 50 /* make this configurable? */ + +typedef struct imap { + int uidnext; /* from SELECT responses */ + list_t *ns_personal, *ns_other, *ns_shared; /* NAMESPACE info */ + string_list_t *boxes; /* LIST results */ + message_t **msgapp; /* FETCH results */ + unsigned caps, rcaps; /* CAPABILITY results */ + /* command queue */ + int nexttag, num_in_progress, literal_pending; + struct imap_cmd *in_progress, **in_progress_append; +#if HAVE_LIBSSL + SSL_CTX *SSLContext; +#endif + buffer_t buf; /* this is BIG, so put it last */ +} imap_t; + +typedef struct imap_store { + store_t gen; + imap_t *imap; + const char *prefix; + unsigned /*currentnc:1,*/ trashnc:1; +} imap_store_t; + +struct imap_cmd_cb { + int (*cont)( imap_store_t *ctx, struct imap_cmd *cmd, const char *prompt ); + void (*done)( imap_store_t *ctx, struct imap_cmd *cmd, int response); + void *ctx; + char *data; + int dlen; + int uid; + unsigned create:1; +}; + +struct imap_cmd { + struct imap_cmd *next; + struct imap_cmd_cb cb; + char *cmd; + int tag; +}; + +#define CAP(cap) (imap->caps & (1 << (cap))) + +enum CAPABILITY { + NOLOGIN = 0, + UIDPLUS, + LITERALPLUS, + NAMESPACE, +#if HAVE_LIBSSL + CRAM, + STARTTLS, +#endif +}; + +static const char *cap_list[] = { + "LOGINDISABLED", + "UIDPLUS", + "LITERAL+", + "NAMESPACE", +#if HAVE_LIBSSL + "AUTH=CRAM-MD5", + "STARTTLS", +#endif +}; + +#define RESP_OK 0 +#define RESP_NO 1 +#define RESP_BAD 2 + +static int get_cmd_result( imap_store_t *ctx, struct imap_cmd *tcmd ); + + +static const char *Flags[] = { + "\\Draft", + "\\Flagged", + "\\Answered", + "\\Seen", + "\\Deleted", +}; + +#if HAVE_LIBSSL + +/* this gets called when a certificate is to be verified */ +static int +verify_cert( SSL *ssl ) +{ + X509 *cert; + int err; + char buf[256]; + int ret = -1; + BIO *bio; + + cert = SSL_get_peer_certificate( ssl ); + if (!cert) { + fprintf( stderr, "Error, no server certificate\n" ); + return -1; + } + + err = SSL_get_verify_result( ssl ); + if (err == X509_V_OK) + return 0; + + fprintf( stderr, "Error, can't verify certificate: %s (%d)\n", + X509_verify_cert_error_string(err), err ); + + X509_NAME_oneline( X509_get_subject_name( cert ), buf, sizeof(buf) ); + info( "\nSubject: %s\n", buf ); + X509_NAME_oneline( X509_get_issuer_name( cert ), buf, sizeof(buf) ); + info( "Issuer: %s\n", buf ); + bio = BIO_new( BIO_s_mem() ); + ASN1_TIME_print( bio, X509_get_notBefore( cert ) ); + memset( buf, 0, sizeof(buf) ); + BIO_read( bio, buf, sizeof(buf) - 1 ); + info( "Valid from: %s\n", buf ); + ASN1_TIME_print( bio, X509_get_notAfter( cert ) ); + memset( buf, 0, sizeof(buf) ); + BIO_read( bio, buf, sizeof(buf) - 1 ); + BIO_free( bio ); + info( " to: %s\n", buf ); + + fputs( "\n*** WARNING *** There is no way to verify this certificate. It is\n" + " possible that a hostile attacker has replaced the\n" + " server certificate. Continue at your own risk!\n" + "\nAccept this certificate anyway? [no]: ", stderr ); + if (fgets( buf, sizeof(buf), stdin ) && (buf[0] == 'y' || buf[0] == 'Y')) { + ret = 0; + fprintf( stderr, "\n*** Fine, but don't say I didn't warn you!\n\n" ); + } + return ret; +} + +static int +init_ssl_ctx( imap_store_t *ctx ) +{ + imap_t *imap = ctx->imap; + imap_store_conf_t *conf = (imap_store_conf_t *)ctx->gen.conf; + imap_server_conf_t *srvc = conf->server; + SSL_METHOD *method; + int options = 0; + + if (srvc->use_tlsv1 && !srvc->use_sslv2 && !srvc->use_sslv3) + method = TLSv1_client_method(); + else + method = SSLv23_client_method(); + imap->SSLContext = SSL_CTX_new( method ); + + if (!srvc->cert_file) { + fprintf( stderr, "Error, CertificateFile not defined\n" ); + return -1; + } else if (access( srvc->cert_file, R_OK )) + warn( "*** Warning: can't read CertificateFile, so can't verify server certificates\n" ); + else if (!SSL_CTX_load_verify_locations( imap->SSLContext, srvc->cert_file, NULL )) { + fprintf( stderr, "Error, SSL_CTX_load_verify_locations: %s\n", + ERR_error_string( ERR_get_error(), 0 ) ); + return -1; + } + + if (!srvc->use_sslv2) + options |= SSL_OP_NO_SSLv2; + if (!srvc->use_sslv3) + options |= SSL_OP_NO_SSLv3; + if (!srvc->use_tlsv1) + options |= SSL_OP_NO_TLSv1; + + SSL_CTX_set_options( imap->SSLContext, options ); + + /* we check the result of the verification after SSL_connect() */ + SSL_CTX_set_verify( imap->SSLContext, SSL_VERIFY_NONE, 0 ); + return 0; +} +#endif /* HAVE_LIBSSL */ + +static void +socket_perror( const char *func, Socket_t *sock, int ret ) +{ +#if HAVE_LIBSSL + int err; + + if (sock->use_ssl) { + switch ((err = SSL_get_error( sock->ssl, ret ))) { + case SSL_ERROR_SYSCALL: + case SSL_ERROR_SSL: + if ((err = ERR_get_error()) == 0) { + if (ret == 0) + fprintf( stderr, "SSL_%s:got EOF\n", func ); + else + fprintf( stderr, "SSL_%s:%d:%s\n", func, errno, strerror(errno) ); + } else + fprintf( stderr, "SSL_%s:%d:%s\n", func, err, ERR_error_string( err, 0 ) ); + return; + default: + fprintf( stderr, "SSL_%s:%d:unhandled SSL error\n", func, err ); + break; + } + return; + } +#else + (void)sock; +#endif + if (ret < 0) + perror( func ); + else + fprintf( stderr, "%s: unexpected EOF\n", func ); +} + +static int +socket_read( Socket_t *sock, char *buf, int len ) +{ + int n = +#if HAVE_LIBSSL + sock->use_ssl ? SSL_read( sock->ssl, buf, len ) : +#endif + read( sock->fd, buf, len ); + if (n <= 0) { + socket_perror( "read", sock, n ); + close( sock->fd ); + sock->fd = -1; + } + return n; +} + +static int +socket_write( Socket_t *sock, char *buf, int len ) +{ + int n = +#if HAVE_LIBSSL + sock->use_ssl ? SSL_write( sock->ssl, buf, len ) : +#endif + write( sock->fd, buf, len ); + if (n != len) { + socket_perror( "write", sock, n ); + close( sock->fd ); + sock->fd = -1; + } + return n; +} + +static int +socket_pending( Socket_t *sock ) +{ + int num = -1; + + if (ioctl( sock->fd, FIONREAD, &num ) < 0) + return -1; + if (num > 0) + return num; +#if HAVE_LIBSSL + if (sock->use_ssl) + return SSL_pending( sock->ssl ); +#endif + return 0; +} + +/* simple line buffering */ +static int +buffer_gets( buffer_t * b, char **s ) +{ + int n; + int start = b->offset; + + *s = b->buf + start; + + for (;;) { + /* make sure we have enough data to read the \r\n sequence */ + if (b->offset + 1 >= b->bytes) { + if (start) { + /* shift down used bytes */ + *s = b->buf; + + assert( start <= b->bytes ); + n = b->bytes - start; + + if (n) + memcpy( b->buf, b->buf + start, n ); + b->offset -= start; + b->bytes = n; + start = 0; + } + + n = socket_read( &b->sock, b->buf + b->bytes, + sizeof(b->buf) - b->bytes ); + + if (n <= 0) + return -1; + + b->bytes += n; + } + + if (b->buf[b->offset] == '\r') { + assert( b->offset + 1 < b->bytes ); + if (b->buf[b->offset + 1] == '\n') { + b->buf[b->offset] = 0; /* terminate the string */ + b->offset += 2; /* next line */ + if (Verbose) + puts( *s ); + return 0; + } + } + + b->offset++; + } + /* not reached */ +} + +static struct imap_cmd * +v_issue_imap_cmd( imap_store_t *ctx, struct imap_cmd_cb *cb, + const char *fmt, va_list ap ) +{ + imap_t *imap = ctx->imap; + struct imap_cmd *cmd; + int n, bufl; + char buf[1024]; + + cmd = nfmalloc( sizeof(struct imap_cmd) ); + nfvasprintf( &cmd->cmd, fmt, ap ); + cmd->tag = ++imap->nexttag; + + if (cb) + cmd->cb = *cb; + else + memset( &cmd->cb, 0, sizeof(cmd->cb) ); + + while (imap->literal_pending) + get_cmd_result( ctx, 0 ); + + bufl = nfsnprintf( buf, sizeof(buf), cmd->cb.data ? CAP(LITERALPLUS) ? + "%d %s{%d+}\r\n" : "%d %s{%d}\r\n" : "%d %s\r\n", + cmd->tag, cmd->cmd, cmd->cb.dlen ); + if (Verbose) { + if (imap->num_in_progress) + printf( "(%d in progress) ", imap->num_in_progress ); + if (memcmp( cmd->cmd, "LOGIN", 5 )) + printf( ">>> %s", buf ); + else + printf( ">>> %d LOGIN <user> <pass>\n", cmd->tag ); + } + if (socket_write( &imap->buf.sock, buf, bufl ) != bufl) { + free( cmd->cmd ); + free( cmd ); + if (cb && cb->data) + free( cb->data ); + return NULL; + } + if (cmd->cb.data) { + if (CAP(LITERALPLUS)) { + n = socket_write( &imap->buf.sock, cmd->cb.data, cmd->cb.dlen ); + free( cmd->cb.data ); + if (n != cmd->cb.dlen || + (n = socket_write( &imap->buf.sock, "\r\n", 2 )) != 2) + { + free( cmd->cmd ); + free( cmd ); + return NULL; + } + cmd->cb.data = 0; + } else + imap->literal_pending = 1; + } else if (cmd->cb.cont) + imap->literal_pending = 1; + cmd->next = 0; + *imap->in_progress_append = cmd; + imap->in_progress_append = &cmd->next; + imap->num_in_progress++; + return cmd; +} + +static struct imap_cmd * +issue_imap_cmd( imap_store_t *ctx, struct imap_cmd_cb *cb, const char *fmt, ... ) +{ + struct imap_cmd *ret; + va_list ap; + + va_start( ap, fmt ); + ret = v_issue_imap_cmd( ctx, cb, fmt, ap ); + va_end( ap ); + return ret; +} + +static struct imap_cmd * +issue_imap_cmd_w( imap_store_t *ctx, struct imap_cmd_cb *cb, const char *fmt, ... ) +{ + imap_t *imap = ctx->imap; + struct imap_cmd *ret; + va_list ap; + + va_start( ap, fmt ); + ret = v_issue_imap_cmd( ctx, cb, fmt, ap ); + va_end( ap ); + while (imap->num_in_progress > max_in_progress || + socket_pending( &imap->buf.sock )) + get_cmd_result( ctx, 0 ); + return ret; +} + +static int +imap_exec( imap_store_t *ctx, struct imap_cmd_cb *cb, const char *fmt, ... ) +{ + va_list ap; + struct imap_cmd *cmdp; + + va_start( ap, fmt ); + cmdp = v_issue_imap_cmd( ctx, cb, fmt, ap ); + va_end( ap ); + if (!cmdp) + return RESP_BAD; + + return get_cmd_result( ctx, cmdp ); +} + +static int +imap_exec_b( imap_store_t *ctx, struct imap_cmd_cb *cb, const char *fmt, ... ) +{ + va_list ap; + struct imap_cmd *cmdp; + + va_start( ap, fmt ); + cmdp = v_issue_imap_cmd( ctx, cb, fmt, ap ); + va_end( ap ); + if (!cmdp) + return DRV_STORE_BAD; + + switch (get_cmd_result( ctx, cmdp )) { + case RESP_BAD: return DRV_STORE_BAD; + case RESP_NO: return DRV_BOX_BAD; + default: return DRV_OK; + } +} + +static int +imap_exec_m( imap_store_t *ctx, struct imap_cmd_cb *cb, const char *fmt, ... ) +{ + va_list ap; + struct imap_cmd *cmdp; + + va_start( ap, fmt ); + cmdp = v_issue_imap_cmd( ctx, cb, fmt, ap ); + va_end( ap ); + if (!cmdp) + return DRV_STORE_BAD; + + switch (get_cmd_result( ctx, cmdp )) { + case RESP_BAD: return DRV_STORE_BAD; + case RESP_NO: return DRV_MSG_BAD; + default: return DRV_OK; + } +} + +/* +static void +drain_imap_replies( imap_t *imap ) +{ + while (imap->num_in_progress) + get_cmd_result( imap, 0 ); +} +*/ + +static int +is_atom( list_t *list ) +{ + return list && list->val && list->val != NIL && list->val != LIST; +} + +static int +is_list( list_t *list ) +{ + return list && list->val == LIST; +} + +static void +free_list( list_t *list ) +{ + list_t *tmp; + + for (; list; list = tmp) { + tmp = list->next; + if (is_list( list )) + free_list( list->child ); + else if (is_atom( list )) + free( list->val ); + free( list ); + } +} + +static int +parse_imap_list_l( imap_t *imap, char **sp, list_t **curp, int level ) +{ + list_t *cur; + char *s = *sp, *p; + int n, bytes; + + for (;;) { + while (isspace( (unsigned char)*s )) + s++; + if (level && *s == ')') { + s++; + break; + } + *curp = cur = nfmalloc( sizeof(*cur) ); + curp = &cur->next; + cur->val = 0; /* for clean bail */ + if (*s == '(') { + /* sublist */ + s++; + cur->val = LIST; + if (parse_imap_list_l( imap, &s, &cur->child, level + 1 )) + goto bail; + } else if (imap && *s == '{') { + /* literal */ + bytes = cur->len = strtol( s + 1, &s, 10 ); + if (*s != '}') + goto bail; + + s = cur->val = nfmalloc( cur->len ); + + /* 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; + + memcpy( s, imap->buf.buf + imap->buf.offset, n ); + s += n; + bytes -= n; + + /* mark that we used part of the buffer */ + imap->buf.offset += n; + + /* now read the rest of the message */ + while (bytes > 0) { + if ((n = socket_read (&imap->buf.sock, s, bytes)) <= 0) + goto bail; + s += n; + bytes -= n; + } + + if (buffer_gets( &imap->buf, &s )) + goto bail; + } else if (*s == '"') { + /* quoted string */ + s++; + p = s; + for (; *s != '"'; s++) + if (!*s) + goto bail; + cur->len = s - p; + s++; + cur->val = nfmalloc( cur->len + 1 ); + memcpy( cur->val, p, cur->len ); + cur->val[cur->len] = 0; + } else { + /* atom */ + p = s; + for (; *s && !isspace( (unsigned char)*s ); s++) + if (level && *s == ')') + break; + cur->len = s - p; + if (cur->len == 3 && !memcmp ("NIL", p, 3)) + cur->val = NIL; + else { + cur->val = nfmalloc( cur->len + 1 ); + memcpy( cur->val, p, cur->len ); + cur->val[cur->len] = 0; + } + } + + if (!level) + break; + if (!*s) + goto bail; + } + *sp = s; + *curp = 0; + return 0; + + bail: + *curp = 0; + return -1; +} + +static list_t * +parse_imap_list( imap_t *imap, char **sp ) +{ + list_t *head; + + if (!parse_imap_list_l( imap, sp, &head, 0 )) + return head; + free_list( head ); + return NULL; +} + +static list_t * +parse_list( char **sp ) +{ + return parse_imap_list( 0, sp ); +} + +static int +parse_fetch( imap_t *imap, char *cmd ) /* move this down */ +{ + list_t *tmp, *list, *flags; + char *body = 0; + imap_message_t *cur; + msg_data_t *msgdata; + struct imap_cmd *cmdp; + int uid = 0, mask = 0, status = 0, size = 0; + unsigned i; + + list = parse_imap_list( imap, &cmd ); + + if (!is_list( list )) { + fprintf( stderr, "IMAP error: bogus FETCH response\n" ); + free_list( list ); + return -1; + } + + for (tmp = list->child; tmp; tmp = tmp->next) { + if (is_atom( tmp )) { + if (!strcmp( "UID", tmp->val )) { + tmp = tmp->next; + if (is_atom( tmp )) + uid = atoi( tmp->val ); + else + fprintf( stderr, "IMAP error: unable to parse UID\n" ); + } else if (!strcmp( "FLAGS", tmp->val )) { + tmp = tmp->next; + if (is_list( tmp )) { + for (flags = tmp->child; flags; flags = flags->next) { + if (is_atom( flags )) { + if (!strcmp( "\\Recent", flags->val )) { + status |= M_RECENT; + goto flagok; + } + for (i = 0; i < as(Flags); i++) + if (!strcmp( Flags[i], flags->val )) { + mask |= 1 << i; + goto flagok; + } + fprintf( stderr, "IMAP warning: unknown flag %s\n", flags->val ); + flagok: ; + } else + fprintf( stderr, "IMAP error: unable to parse FLAGS list\n" ); + } + status |= M_FLAGS; + } else + fprintf( stderr, "IMAP error: unable to parse FLAGS\n" ); + } else if (!strcmp( "RFC822.SIZE", tmp->val )) { + tmp = tmp->next; + if (is_atom( tmp )) + size = atoi( tmp->val ); + else + fprintf( stderr, "IMAP error: unable to parse SIZE\n" ); + } else if (!strcmp( "BODY[]", tmp->val )) { + tmp = tmp->next; + if (is_atom( tmp )) { + body = tmp->val; + tmp->val = 0; /* don't free together with list */ + size = tmp->len; + } else + fprintf( stderr, "IMAP error: unable to parse BODY[]\n" ); + } + } + } + + if (body) { + for (cmdp = imap->in_progress; cmdp; cmdp = cmdp->next) + if (cmdp->cb.uid == uid) + goto gotuid; + fprintf( stderr, "IMAP error: unexpected FETCH response (UID %d)\n", uid ); + free_list( list ); + return -1; + gotuid: + msgdata = (msg_data_t *)cmdp->cb.ctx; + msgdata->data = body; + msgdata->len = size; + msgdata->crlf = 1; + if (status & M_FLAGS) + msgdata->flags = mask; + } else if (uid) { /* ignore async flag updates for now */ + /* XXX this will need sorting for out-of-order (multiple queries) */ + cur = nfcalloc( sizeof(*cur) ); + *imap->msgapp = &cur->gen; + imap->msgapp = &cur->gen.next; + cur->gen.next = 0; + cur->gen.uid = uid; + cur->gen.flags = mask; + cur->gen.status = status; + cur->gen.size = size; + } + + free_list( list ); + return 0; +} + +static void +parse_capability( imap_t *imap, char *cmd ) +{ + char *arg; + unsigned i; + + imap->caps = 0x80000000; + while ((arg = next_arg( &cmd ))) + for (i = 0; i < as(cap_list); i++) + if (!strcmp( cap_list[i], arg )) + imap->caps |= 1 << i; + imap->rcaps = imap->caps; +} + +static int +parse_response_code( imap_store_t *ctx, struct imap_cmd_cb *cb, char *s ) +{ + imap_t *imap = ctx->imap; + char *arg, *p; + + if (*s != '[') + return RESP_OK; /* no response code */ + s++; + if (!(p = strchr( s, ']' ))) { + fprintf( stderr, "IMAP error: malformed response code\n" ); + return RESP_BAD; + } + *p++ = 0; + arg = next_arg( &s ); + if (!strcmp( "UIDVALIDITY", arg )) { + if (!(arg = next_arg( &s )) || !(ctx->gen.uidvalidity = atoi( arg ))) { + fprintf( stderr, "IMAP error: malformed UIDVALIDITY status\n" ); + return RESP_BAD; + } + } else if (!strcmp( "UIDNEXT", arg )) { + if (!(arg = next_arg( &s )) || !(imap->uidnext = atoi( arg ))) { + fprintf( stderr, "IMAP error: malformed NEXTUID status\n" ); + return RESP_BAD; + } + } else if (!strcmp( "CAPABILITY", arg )) { + parse_capability( imap, s ); + } else if (!strcmp( "ALERT", arg )) { + /* RFC2060 says that these messages MUST be displayed + * to the user + */ + for (; isspace( (unsigned char)*p ); p++); + fprintf( stderr, "*** IMAP ALERT *** %s\n", p ); + } else if (cb && cb->ctx && !strcmp( "APPENDUID", arg )) { + if (!(arg = next_arg( &s )) || !(ctx->gen.uidvalidity = atoi( arg )) || + !(arg = next_arg( &s )) || !(*(int *)cb->ctx = atoi( arg ))) + { + fprintf( stderr, "IMAP error: malformed APPENDUID status\n" ); + return RESP_BAD; + } + } + return RESP_OK; +} + +static void +parse_search( imap_t *imap, char *cmd ) +{ + char *arg; + struct imap_cmd *cmdp; + int uid; + + arg = next_arg( &cmd ); + if (!arg || !(uid = atoi( arg ))) { + fprintf( stderr, "IMAP error: malformed SEARCH response\n" ); + return; + } + + /* Find the first command that expects a UID - this is guaranteed + * to come in-order, as there are no other means to identify which + * SEARCH response belongs to which request. + */ + for (cmdp = imap->in_progress; cmdp; cmdp = cmdp->next) + if (cmdp->cb.uid == -1) { + *(int *)cmdp->cb.ctx = uid; + return; + } + fprintf( stderr, "IMAP error: unexpected SEARCH response (UID %u)\n", uid ); +} + +static void +parse_list_rsp( imap_store_t *ctx, char *cmd ) +{ + imap_t *imap = ctx->imap; + char *arg; + list_t *list, *lp; + int l; + + list = parse_list( &cmd ); + if (list->val == LIST) + for (lp = list->child; lp; lp = lp->next) + if (is_atom( lp ) && !strcasecmp( lp->val, "\\NoSelect" )) { + free_list( list ); + return; + } + free_list( list ); + (void) next_arg( &cmd ); /* skip delimiter */ + arg = next_arg( &cmd ); + l = strlen( ctx->gen.conf->path ); + if (memcmp( arg, ctx->gen.conf->path, l )) + return; + arg += l; + if (!memcmp( arg + strlen( arg ) - 5, ".lock", 5 )) /* workaround broken servers */ + return; + add_string_list( &imap->boxes, arg ); +} + +static int +get_cmd_result( imap_store_t *ctx, struct imap_cmd *tcmd ) +{ + imap_t *imap = ctx->imap; + struct imap_cmd *cmdp, **pcmdp, *ncmdp; + char *cmd, *arg, *arg1, *p; + int n, resp, resp2, tag; + + for (;;) { + if (buffer_gets( &imap->buf, &cmd )) + return RESP_BAD; + + arg = next_arg( &cmd ); + if (*arg == '*') { + arg = next_arg( &cmd ); + if (!arg) { + fprintf( stderr, "IMAP error: unable to parse untagged response\n" ); + return RESP_BAD; + } + + if (!strcmp( "NAMESPACE", arg )) { + imap->ns_personal = parse_list( &cmd ); + imap->ns_other = parse_list( &cmd ); + imap->ns_shared = parse_list( &cmd ); + } else if (!strcmp( "OK", arg ) || !strcmp( "BAD", arg ) || + !strcmp( "NO", arg ) || !strcmp( "BYE", arg )) { + if ((resp = parse_response_code( ctx, 0, cmd )) != RESP_OK) + return resp; + } else if (!strcmp( "CAPABILITY", arg )) + parse_capability( imap, cmd ); + else if (!strcmp( "LIST", arg )) + parse_list_rsp( ctx, cmd ); + else if (!strcmp( "SEARCH", arg )) + parse_search( imap, cmd ); + else if ((arg1 = next_arg( &cmd ))) { + if (!strcmp( "EXISTS", arg1 )) + ctx->gen.count = atoi( arg ); + else if (!strcmp( "RECENT", arg1 )) + ctx->gen.recent = atoi( arg ); + else if(!strcmp ( "FETCH", arg1 )) { + if (parse_fetch( imap, cmd )) + return RESP_BAD; + } + } else { + fprintf( stderr, "IMAP error: unable to parse untagged response\n" ); + return RESP_BAD; + } + } else if (!imap->in_progress) { + fprintf( stderr, "IMAP error: unexpected reply: %s %s\n", arg, cmd ? cmd : "" ); + return RESP_BAD; + } else if (*arg == '+') { + /* This can happen only with the last command underway, as + it enforces a round-trip. */ + cmdp = (struct imap_cmd *)((char *)imap->in_progress_append - + offsetof(struct imap_cmd, next)); + if (cmdp->cb.data) { + n = socket_write( &imap->buf.sock, cmdp->cb.data, cmdp->cb.dlen ); + free( cmdp->cb.data ); + cmdp->cb.data = 0; + if (n != (int)cmdp->cb.dlen) + return RESP_BAD; + } else if (cmdp->cb.cont) { + if (cmdp->cb.cont( ctx, cmdp, cmd )) + return RESP_BAD; + } else { + fprintf( stderr, "IMAP error: unexpected command continuation request\n" ); + return RESP_BAD; + } + if (socket_write( &imap->buf.sock, "\r\n", 2 ) != 2) + return RESP_BAD; + if (!cmdp->cb.cont) + imap->literal_pending = 0; + if (!tcmd) + return DRV_OK; + } else { + tag = atoi( arg ); + for (pcmdp = &imap->in_progress; (cmdp = *pcmdp); pcmdp = &cmdp->next) + if (cmdp->tag == tag) + goto gottag; + fprintf( stderr, "IMAP error: unexpected tag %s\n", arg ); + return RESP_BAD; + gottag: + if (!(*pcmdp = cmdp->next)) + imap->in_progress_append = pcmdp; + imap->num_in_progress--; + if (cmdp->cb.cont || cmdp->cb.data) + imap->literal_pending = 0; + arg = next_arg( &cmd ); + if (!strcmp( "OK", arg )) + resp = DRV_OK; + else { + if (!strcmp( "NO", arg )) { + if (cmdp->cb.create && cmd && !memcmp( cmd, "[TRYCREATE]", 11 )) { /* SELECT, APPEND or UID COPY */ + p = strchr( cmdp->cmd, '"' ); + if (!issue_imap_cmd( ctx, 0, "CREATE %.*s", strchr( p + 1, '"' ) - p + 1, p )) { + resp = RESP_BAD; + goto normal; + } + /* not waiting here violates the spec, but a server that does not + grok this nonetheless violates it too. */ + cmdp->cb.create = 0; + if (!(ncmdp = issue_imap_cmd( ctx, &cmdp->cb, "%s", cmdp->cmd ))) { + resp = RESP_BAD; + goto normal; + } + free( cmdp->cmd ); + free( cmdp ); + if (!tcmd) + return 0; /* ignored */ + if (cmdp == tcmd) + tcmd = ncmdp; + continue; + } + resp = RESP_NO; + } else /*if (!strcmp( "BAD", arg ))*/ + resp = RESP_BAD; + fprintf( stderr, "IMAP command '%s' returned an error: %s %s\n", + memcmp (cmdp->cmd, "LOGIN", 5) ? + cmdp->cmd : "LOGIN <user> <pass>", + arg, cmd ? cmd : ""); + } + if ((resp2 = parse_response_code( ctx, cmdp->cb.ctx, cmd )) > resp) + resp = resp2; + normal: + if (cmdp->cb.done) + cmdp->cb.done( ctx, cmdp, resp ); + if (cmdp->cb.data) + free( cmdp->cb.data ); + free( cmdp->cmd ); + free( cmdp ); + if (!tcmd || tcmd == cmdp) + return resp; + } + } + /* not reached */ +} + +static void +imap_close_server( imap_store_t *ictx ) +{ + imap_t *imap = ictx->imap; + + if (imap->buf.sock.fd != -1) { + imap_exec( ictx, 0, "LOGOUT" ); + close( imap->buf.sock.fd ); + } +#ifdef HAVE_LIBSSL + if (imap->SSLContext) + SSL_CTX_free( imap->SSLContext ); +#endif + free_list( imap->ns_personal ); + free_list( imap->ns_other ); + free_list( imap->ns_shared ); + free( imap ); +} + +static void +imap_close_store( store_t *ctx ) +{ + imap_close_server( (imap_store_t *)ctx ); + free_generic_messages( ctx->msgs ); + free( ctx ); +} + +#ifdef HAVE_LIBSSL +static int +start_tls( imap_store_t *ctx ) +{ + imap_t *imap = ctx->imap; + int ret; + static int ssl_inited; + + if (!ssl_inited) { + SSL_library_init(); + SSL_load_error_strings(); + ssl_inited = 1; + } + + if (init_ssl_ctx( ctx )) + return 1; + + imap->buf.sock.ssl = SSL_new( imap->SSLContext ); + SSL_set_fd( imap->buf.sock.ssl, imap->buf.sock.fd ); + if ((ret = SSL_connect( imap->buf.sock.ssl )) <= 0) { + socket_perror( "connect", &imap->buf.sock, ret ); + return 1; + } + + /* verify the server certificate */ + if (verify_cert( imap->buf.sock.ssl )) + return 1; + + imap->buf.sock.use_ssl = 1; + info( "Connection is now encrypted\n" ); + return 0; +} + +#define ENCODED_SIZE(n) (4*((n+2)/3)) + +static char +hexchar( unsigned int b ) +{ + if (b < 10) + return '0' + b; + return 'a' + (b - 10); +} + +/* XXX merge into do_cram_auth? */ +static char * +cram( const char *challenge, const char *user, const char *pass ) +{ + HMAC_CTX hmac; + char hash[16]; + char hex[33]; + int i; + unsigned int hashlen = sizeof(hash); + char buf[256]; + int len = strlen( challenge ); + char *response = nfcalloc( 1 + len ); + char *final; + + /* response will always be smaller than challenge because we are + * decoding. + */ + len = EVP_DecodeBlock( (unsigned char *)response, (unsigned char *)challenge, strlen( challenge ) ); + + HMAC_Init( &hmac, (unsigned char *) pass, strlen( pass ), EVP_md5() ); + HMAC_Update( &hmac, (unsigned char *)response, strlen( response ) ); + HMAC_Final( &hmac, (unsigned char *)hash, &hashlen ); + + assert( hashlen == sizeof(hash) ); + + free( response ); + + hex[32] = 0; + for (i = 0; i < 16; i++) { + hex[2 * i] = hexchar( (hash[i] >> 4) & 0xf ); + hex[2 * i + 1] = hexchar( hash[i] & 0xf ); + } + + nfsnprintf( buf, sizeof(buf), "%s %s", user, hex ); + + len = strlen( buf ); + len = ENCODED_SIZE( len ) + 1; + final = nfmalloc( len ); + final[len - 1] = 0; + + assert( EVP_EncodeBlock( (unsigned char *)final, (unsigned char *)buf, strlen( buf ) ) == len - 1 ); + + return final; +} + +static int +do_cram_auth (imap_store_t *ctx, struct imap_cmd *cmdp, const char *prompt) +{ + imap_t *imap = ctx->imap; + imap_server_conf_t *srvc = ((imap_store_conf_t *)ctx->gen.conf)->server; + char *resp; + int n, l; + + resp = cram( prompt, srvc->user, srvc->pass ); + + if (Verbose) + printf( ">+> %s\n", resp ); + l = strlen( resp ); + n = socket_write( &imap->buf.sock, resp, l ); + free( resp ); + if (n != l) + return -1; + if (socket_write( &imap->buf.sock, "\r\n", 2 ) != 2) + return -1; + cmdp->cb.cont = 0; + return 0; +} +#endif + +static store_t * +imap_open_store( store_conf_t *conf, store_t *oldctx ) +{ + imap_store_conf_t *cfg = (imap_store_conf_t *)conf; + imap_server_conf_t *srvc = cfg->server; + imap_store_t *ctx = (imap_store_t *)oldctx; + imap_t *imap; + char *arg, *rsp; + struct hostent *he; + struct sockaddr_in addr; + int s, a[2], preauth; +#if HAVE_LIBSSL + int use_ssl; +#endif + + if (ctx) { + if (((imap_store_conf_t *)(ctx->gen.conf))->server == cfg->server) { + ctx->gen.conf = conf; + imap = ctx->imap; + goto final; + } + imap_close_server( ctx ); + } else + ctx = nfcalloc( sizeof(*ctx) ); + + ctx->gen.conf = conf; + ctx->imap = imap = nfcalloc( sizeof(*imap) ); + imap->buf.sock.fd = -1; + imap->in_progress_append = &imap->in_progress; + + /* open connection to IMAP server */ +#if HAVE_LIBSSL + use_ssl = 0; +#endif + + if (srvc->tunnel) { + info( "Starting tunnel '%s'... ", srvc->tunnel ); + + if (socketpair( PF_UNIX, SOCK_STREAM, 0, a )) { + perror( "socketpair" ); + exit( 1 ); + } + + if (fork() == 0) { + if (dup2( a[0], 0 ) == -1 || dup2( a[0], 1 ) == -1) + _exit( 127 ); + close( a[0] ); + close( a[1] ); + execl( "/bin/sh", "sh", "-c", srvc->tunnel, 0 ); + _exit( 127 ); + } + + close (a[0]); + + imap->buf.sock.fd = a[1]; + + info( "ok\n" ); + } else { + memset( &addr, 0, sizeof(addr) ); + addr.sin_port = htons( srvc->port ); + addr.sin_family = AF_INET; + + info( "Resolving %s... ", srvc->host ); + he = gethostbyname( srvc->host ); + if (!he) { + perror( "gethostbyname" ); + goto bail; + } + info( "ok\n" ); + + addr.sin_addr.s_addr = *((int *) he->h_addr_list[0]); + + s = socket( PF_INET, SOCK_STREAM, 0 ); + + info( "Connecting to %s:%hu... ", inet_ntoa( addr.sin_addr ), ntohs( addr.sin_port ) ); + if (connect( s, (struct sockaddr *)&addr, sizeof(addr) )) { + close( s ); + perror( "connect" ); + goto bail; + } + info( "ok\n" ); + + imap->buf.sock.fd = s; + +#if HAVE_LIBSSL + if (srvc->use_imaps) { + if (start_tls( ctx )) + goto bail; + use_ssl = 1; + } +#endif + } + + /* read the greeting string */ + if (buffer_gets( &imap->buf, &rsp )) { + fprintf( stderr, "IMAP error: no greeting response\n" ); + goto bail; + } + arg = next_arg( &rsp ); + if (!arg || *arg != '*' || (arg = next_arg( &rsp )) == NULL) { + fprintf( stderr, "IMAP error: invalid greeting response\n" ); + goto bail; + } + preauth = 0; + if (!strcmp( "PREAUTH", arg )) + preauth = 1; + else if (strcmp( "OK", arg ) != 0) { + fprintf( stderr, "IMAP error: unknown greeting response\n" ); + goto bail; + } + parse_response_code( ctx, 0, rsp ); + if (!imap->caps && imap_exec( ctx, 0, "CAPABILITY" ) != RESP_OK) + goto bail; + + if (!preauth) { +#if HAVE_LIBSSL + if (!srvc->use_imaps && (srvc->use_sslv2 || srvc->use_sslv3 || srvc->use_tlsv1)) { + /* always try to select SSL support if available */ + if (CAP(STARTTLS)) { + if (imap_exec( ctx, 0, "STARTTLS" ) != RESP_OK) + goto bail; + if (start_tls( ctx )) + goto bail; + use_ssl = 1; + + if (imap_exec( ctx, 0, "CAPABILITY" ) != RESP_OK) + goto bail; + } else { + if (srvc->require_ssl) { + fprintf( stderr, "IMAP error: SSL support not available\n" ); + goto bail; + } else + warn( "IMAP warning: SSL support not available\n" ); + } + } +#endif + + info ("Logging in...\n"); + if (!srvc->user) { + fprintf( stderr, "Skipping server %s, no user\n", srvc->host ); + goto bail; + } + if (!srvc->pass) { + char prompt[80]; + sprintf( prompt, "Password (%s@%s): ", srvc->user, srvc->host ); + arg = getpass( prompt ); + if (!arg) { + perror( "getpass" ); + exit( 1 ); + } + if (!*arg) { + fprintf( stderr, "Skipping account %s@%s, no password\n", srvc->user, srvc->host ); + goto bail; + } + /* + * getpass() returns a pointer to a static buffer. make a copy + * for long term storage. + */ + srvc->pass = nfstrdup( arg ); + } +#if HAVE_LIBSSL + if (CAP(CRAM)) { + struct imap_cmd_cb cb; + + info( "Authenticating with CRAM-MD5\n" ); + memset( &cb, 0, sizeof(cb) ); + cb.cont = do_cram_auth; + if (imap_exec( ctx, &cb, "AUTHENTICATE CRAM-MD5" ) != RESP_OK) + goto bail; + } else if (srvc->require_cram) { + fprintf( stderr, "IMAP error: CRAM-MD5 authentication is not supported by server\n" ); + goto bail; + } else +#endif + { + if (CAP(NOLOGIN)) { + fprintf( stderr, "Skipping account %s@%s, server forbids LOGIN\n", srvc->user, srvc->host ); + goto bail; + } +#if HAVE_LIBSSL + if (!use_ssl) +#endif + warn( "*** IMAP Warning *** Password is being sent in the clear\n" ); + if (imap_exec( ctx, 0, "LOGIN \"%s\" \"%s\"", srvc->user, srvc->pass ) != RESP_OK) { + fprintf( stderr, "IMAP error: LOGIN failed\n" ); + goto bail; + } + } + } /* !preauth */ + + final: + ctx->prefix = ""; + if (*conf->path) + ctx->prefix = conf->path; + else if (cfg->use_namespace && CAP(NAMESPACE)) { + /* get NAMESPACE info */ + if (imap_exec( ctx, 0, "NAMESPACE" ) != RESP_OK) + goto bail; + /* XXX for now assume personal namespace */ + if (is_list( imap->ns_personal ) && + is_list( imap->ns_personal->child ) && + is_atom( imap->ns_personal->child->child )) + ctx->prefix = imap->ns_personal->child->child->val; + } + ctx->trashnc = 1; + return (store_t *)ctx; + + bail: + imap_close_store( &ctx->gen ); + return 0; +} + +static void +imap_prepare( store_t *gctx, int opts ) +{ + free_generic_messages( gctx->msgs ); + gctx->msgs = 0; + gctx->opts = opts; +} + +static int +imap_select( store_t *gctx, int minuid, int maxuid, int *excs, int nexcs ) +{ + imap_store_t *ctx = (imap_store_t *)gctx; + imap_t *imap = ctx->imap; + const char *prefix; + int ret, i, j, bl; + struct imap_cmd_cb cb; + char buf[1000]; + + + if (!strcmp( gctx->name, "INBOX" )) { +// imap->currentnc = 0; + prefix = ""; + } else { +// imap->currentnc = 1; /* could use LIST results for that */ + prefix = ctx->prefix; + } + + memset( &cb, 0, sizeof(cb) ); + cb.create = gctx->opts & OPEN_CREATE; + if ((ret = imap_exec_b( ctx, &cb, "SELECT \"%s%s\"", prefix, gctx->name )) != DRV_OK) + goto bail; + + if (gctx->count) { + imap->msgapp = &gctx->msgs; + sort_ints( excs, nexcs ); + for (i = 0; i < nexcs; ) { + for (bl = 0; i < nexcs && bl < 960; i++) { + if (bl) + buf[bl++] = ','; + bl += sprintf( buf + bl, "%d", excs[i] ); + j = i; + for (; i + 1 < nexcs && excs[i + 1] == excs[i] + 1; i++); + if (i != j) + bl += sprintf( buf + bl, ":%d", excs[i] ); + } + if ((ret = imap_exec_b( ctx, 0, "UID FETCH %s (UID%s%s)", buf, + (gctx->opts & OPEN_FLAGS) ? " FLAGS" : "", + (gctx->opts & OPEN_SIZE) ? " RFC822.SIZE" : "" )) != DRV_OK) + goto bail; + } + if (maxuid == INT_MAX) + maxuid = imap->uidnext ? imap->uidnext - 1 : 1000000000; + if (maxuid >= minuid && + (ret = imap_exec_b( ctx, 0, "UID FETCH %d:%d (UID%s%s)", minuid, maxuid, + (gctx->opts & OPEN_FLAGS) ? " FLAGS" : "", + (gctx->opts & OPEN_SIZE) ? " RFC822.SIZE" : "" )) != DRV_OK) + goto bail; + } + + ret = DRV_OK; + + bail: + if (excs) + free( excs ); + return ret; +} + +static int +imap_fetch_msg( store_t *ctx, message_t *msg, msg_data_t *data ) +{ + struct imap_cmd_cb cb; + + memset( &cb, 0, sizeof(cb) ); + cb.uid = msg->uid; + cb.ctx = data; + return imap_exec_m( (imap_store_t *)ctx, &cb, "UID FETCH %d (%sBODY.PEEK[])", + msg->uid, (msg->status & M_FLAGS) ? "" : "FLAGS " ); +} + +static int +imap_make_flags( int flags, char *buf ) +{ + const char *s; + unsigned i, d; + + for (i = d = 0; i < as(Flags); i++) + if (flags & (1 << i)) { + buf[d++] = ' '; + for (s = Flags[i]; *s; s++) + buf[d++] = *s; + } + buf[0] = '('; + buf[d++] = ')'; + return d; +} + +static int +imap_flags_helper( imap_store_t *ctx, int uid, char what, int flags) +{ + char buf[256]; + + buf[imap_make_flags( flags, buf )] = 0; + return issue_imap_cmd_w( ctx, 0, "UID STORE %d %cFLAGS.SILENT %s", uid, what, buf ) ? DRV_OK : DRV_STORE_BAD; +} + +static int +imap_set_flags( store_t *gctx, message_t *msg, int uid, int add, int del ) +{ + imap_store_t *ctx = (imap_store_t *)gctx; + int ret; + + if (msg) { + uid = msg->uid; + add &= ~msg->flags; + del &= msg->flags; + msg->flags |= add; + msg->flags &= ~del; + } + if ((!add || (ret = imap_flags_helper( ctx, uid, '+', add )) == DRV_OK) && + (!del || (ret = imap_flags_helper( ctx, uid, '-', del )) == DRV_OK)) + return DRV_OK; + return ret; +} + +static int +imap_close( store_t *ctx ) +{ + return imap_exec_b( (imap_store_t *)ctx, 0, "CLOSE" ); +} + +static int +imap_trash_msg( store_t *gctx, message_t *msg ) +{ + imap_store_t *ctx = (imap_store_t *)gctx; + struct imap_cmd_cb cb; + + memset( &cb, 0, sizeof(cb) ); + cb.create = 1; + return imap_exec_m( ctx, &cb, "UID COPY %d \"%s%s\"", + msg->uid, ctx->prefix, gctx->conf->trash ); +} + +#define TUIDL 8 + +static int +imap_store_msg( store_t *gctx, msg_data_t *data, int *uid ) +{ + imap_store_t *ctx = (imap_store_t *)gctx; + imap_t *imap = ctx->imap; + struct imap_cmd_cb cb; + char *fmap, *buf; + const char *prefix, *box; + int ret, i, j, d, len, extra, nocr; + int start, sbreak = 0, ebreak = 0; + char flagstr[128], tuid[TUIDL * 2 + 1]; + + memset( &cb, 0, sizeof(cb) ); + + fmap = data->data; + len = data->len; + nocr = !data->crlf; + extra = 0, i = 0; + if (!CAP(UIDPLUS) && uid) { + nloop: + start = i; + while (i < len) + if (fmap[i++] == '\n') { + extra += nocr; + if (i - 2 + nocr == start) { + sbreak = ebreak = i - 2 + nocr; + goto mktid; + } + if (!memcmp( fmap + start, "X-TUID: ", 8 )) { + extra -= (ebreak = i) - (sbreak = start) + nocr; + goto mktid; + } + goto nloop; + } + /* invalid message */ + free( fmap ); + return DRV_MSG_BAD; + mktid: + for (j = 0; j < TUIDL; j++) + sprintf( tuid + j * 2, "%02x", arc4_getbyte() ); + extra += 8 + TUIDL * 2 + 2; + } + if (nocr) + for (; i < len; i++) + if (fmap[i] == '\n') + extra++; + + cb.dlen = len + extra; + buf = cb.data = nfmalloc( cb.dlen ); + i = 0; + if (!CAP(UIDPLUS) && uid) { + if (nocr) { + for (; i < sbreak; i++) + if (fmap[i] == '\n') { + *buf++ = '\r'; + *buf++ = '\n'; + } else + *buf++ = fmap[i]; + } else { + memcpy( buf, fmap, sbreak ); + buf += sbreak; + } + memcpy( buf, "X-TUID: ", 8 ); + buf += 8; + memcpy( buf, tuid, TUIDL * 2 ); + buf += TUIDL * 2; + *buf++ = '\r'; + *buf++ = '\n'; + i = ebreak; + } + if (nocr) { + for (; i < len; i++) + if (fmap[i] == '\n') { + *buf++ = '\r'; + *buf++ = '\n'; + } else + *buf++ = fmap[i]; + } else + memcpy( buf, fmap + i, len - i ); + + free( fmap ); + + d = 0; + if (data->flags) { + d = imap_make_flags( data->flags, flagstr ); + flagstr[d++] = ' '; + } + flagstr[d] = 0; + + if (!uid) { + box = gctx->conf->trash; + prefix = ctx->prefix; + cb.create = 1; + if (ctx->trashnc) + imap->caps = imap->rcaps & ~(1 << LITERALPLUS); + } else { + box = gctx->name; + prefix = !strcmp( box, "INBOX" ) ? "" : ctx->prefix; + cb.create = gctx->opts & OPEN_CREATE; + /*if (ctx->currentnc) + imap->caps = imap->rcaps & ~(1 << LITERALPLUS);*/ + } + cb.ctx = uid; + ret = imap_exec_m( ctx, &cb, "APPEND \"%s%s\" %s", prefix, box, flagstr ); + imap->caps = imap->rcaps; + if (ret != DRV_OK) + return ret; + if (!uid) + ctx->trashnc = 0; + else { + /*ctx->currentnc = 0;*/ + gctx->count++; + } + + if (CAP(UIDPLUS) || !uid) + return DRV_OK; + + /* Didn't receive an APPENDUID */ + cb.uid = -1; /* we're looking for a UID */ + cb.data = 0; /* reset; ctx still set */ + return imap_exec_m( ctx, &cb, "UID SEARCH HEADER X-TUID %s", tuid ); +} + +static int +imap_list( store_t *gctx, string_list_t **retb ) +{ + imap_store_t *ctx = (imap_store_t *)gctx; + imap_t *imap = ctx->imap; + int ret; + + imap->boxes = 0; + if ((ret = imap_exec_b( ctx, 0, "LIST \"\" \"%s%%\"", ctx->prefix )) != DRV_OK) + return ret; + *retb = imap->boxes; + return DRV_OK; +} + +static int +imap_check( store_t *gctx ) +{ + (void) gctx; + /* flush queue here */ + return DRV_OK; +} + +imap_server_conf_t *servers, **serverapp = &servers; + +static int +imap_parse_store( conffile_t *cfg, store_conf_t **storep, int *err ) +{ + imap_store_conf_t *store; + imap_server_conf_t *server, *srv, sserver; + + if (!strcasecmp( "IMAPAccount", cfg->cmd )) { + server = nfcalloc( sizeof(*server) ); + server->name = nfstrdup( cfg->val ); + *serverapp = server; + serverapp = &server->next; + store = 0; + } else if (!strcasecmp( "IMAPStore", cfg->cmd )) { + store = nfcalloc( sizeof(*store) ); + store->gen.driver = &imap_driver; + store->gen.name = nfstrdup( cfg->val ); + store->use_namespace = 1; + memset( &sserver, 0, sizeof(sserver) ); + server = &sserver; + } else + return 0; + +#if HAVE_LIBSSL + /* this will probably annoy people, but its the best default just in + * case people forget to turn it on + */ + server->require_ssl = 1; + server->use_tlsv1 = 1; +#endif + + while (getcline( cfg ) && cfg->cmd) { + if (!strcasecmp( "Account", cfg->cmd )) { + for (srv = servers; srv; srv = srv->next) + if (srv->name && !strcmp( srv->name, cfg->val )) + goto gotsrv; + fprintf( stderr, "%s:%d: unknown IMAP account '%s'\n", + cfg->file, cfg->line, cfg->val ); + *err = 1; + continue; + gotsrv: + store->server = srv; + } else if (!strcasecmp( "Host", cfg->cmd )) { +#if HAVE_LIBSSL + if (!memcmp( "imaps:", cfg->val, 6 )) { + cfg->val += 6; + server->use_imaps = 1; + server->use_sslv2 = 1; + server->use_sslv3 = 1; + if (!server->port) + server->port = 993; + } else +#endif + { + if (!memcmp( "imap:", cfg->val, 5 )) + cfg->val += 5; + if (!server->port) + server->port = 143; + } + if (!memcmp( "//", cfg->val, 2 )) + cfg->val += 2; + server->host = nfstrdup( cfg->val ); + } + else if (!strcasecmp( "User", cfg->cmd )) + server->user = nfstrdup( cfg->val ); + else if (!strcasecmp( "Pass", cfg->cmd )) + server->pass = nfstrdup( cfg->val ); + else if (!strcasecmp( "Port", cfg->cmd )) + server->port = parse_int( cfg ); +#if HAVE_LIBSSL + else if (!strcasecmp( "CertificateFile", cfg->cmd )) + server->cert_file = expand_strdup( cfg->val ); + else if (!strcasecmp( "RequireSSL", cfg->cmd )) + server->require_ssl = parse_bool( cfg ); + else if (!strcasecmp( "UseSSLv2", cfg->cmd )) + server->use_sslv2 = parse_bool( cfg ); + else if (!strcasecmp( "UseSSLv3", cfg->cmd )) + server->use_sslv3 = parse_bool( cfg ); + else if (!strcasecmp( "UseTLSv1", cfg->cmd )) + server->use_tlsv1 = parse_bool( cfg ); + else if (!strcasecmp( "RequireCRAM", cfg->cmd )) + server->require_cram = parse_bool( cfg ); +#endif + else if (!strcasecmp( "Tunnel", cfg->cmd )) + server->tunnel = nfstrdup( cfg->val ); + else if (store) { + if (!strcasecmp( "UseNamespace", cfg->cmd )) + store->use_namespace = parse_bool( cfg ); + else if (!strcasecmp( "Path", cfg->cmd )) + store->gen.path = nfstrdup( cfg->val ); + else + parse_generic_store( &store->gen, cfg, err ); + } else { + fprintf( stderr, "%s:%d: unknown keyword '%s'\n", + cfg->file, cfg->line, cfg->cmd ); + *err = 1; + } + } + if (!store || !store->server) { + if (!server->tunnel && !server->host) { + if (store) + fprintf( stderr, "IMAP store '%s' has incomplete connection details\n", store->gen.name ); + else + fprintf( stderr, "IMAP account '%s' has incomplete connection details\n", server->name ); + *err = 1; + /* leaking server/store */ + *storep = 0; + return 1; + } + } + *storep = &store->gen; + if (store && !store->server) { + store->server = nfmalloc( sizeof(sserver) ); + memcpy( store->server, &sserver, sizeof(sserver) ); + } + return 1; +} + +struct driver imap_driver = { + imap_parse_store, + imap_open_store, + imap_close_store, + imap_list, + imap_prepare, + imap_select, + imap_fetch_msg, + imap_store_msg, + imap_set_flags, + imap_trash_msg, + imap_check, + imap_close +}; diff --git a/src/drv_maildir.c b/src/drv_maildir.c @@ -0,0 +1,1153 @@ +/* + * mbsync - mailbox synchronizer + * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> + * Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net> + * Copyright (C) 2004 Theodore Y. Ts'o <tytso@mit.edu> + * + * 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 + * + * As a special exception, mbsync may be linked with the OpenSSL library, + * despite that library's more restrictive license. + */ + +#include "isync.h" + +#include <limits.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> +#include <dirent.h> +#include <fcntl.h> +#include <stdio.h> +#include <unistd.h> +#include <sys/stat.h> +#include <sys/file.h> +#include <errno.h> +#include <time.h> + +#define USE_DB 1 + +#ifdef USE_DB +#include <db.h> +#endif /* USE_DB */ + +typedef struct maildir_store_conf { + store_conf_t gen; + char *inbox; +#ifdef USE_DB + int alt_map; +#endif /* USE_DB */ +} maildir_store_conf_t; + +typedef struct maildir_message { + message_t gen; + char *base; +} maildir_message_t; + +typedef struct maildir_store { + store_t gen; + int uvfd, uvok, nuid; + int minuid, maxuid, nexcs, *excs; +#ifdef USE_DB + DB *db; +#endif /* USE_DB */ +} maildir_store_t; + +#ifdef USE_DB +static DBT key, value; /* no need to be reentrant, and this saves lots of memset()s */ +#endif /* USE_DB */ +static struct flock lck; + +static int MaildirCount; + +static const char Flags[] = { 'D', 'F', 'R', 'S', 'T' }; + +static unsigned char +maildir_parse_flags( const char *base ) +{ + const char *s; + unsigned i; + unsigned char flags; + + flags = 0; + if ((s = strstr( base, ":2," ))) + for (s += 3, i = 0; i < as(Flags); i++) + if (strchr( s, Flags[i] )) + flags |= (1 << i); + return flags; +} + +static void maildir_close_store( store_t *gctx ); + +static store_t * +maildir_open_store( store_conf_t *conf, store_t *oldctx ) +{ + maildir_store_t *ctx; + struct stat st; + + if (oldctx) + maildir_close_store( oldctx ); + if (stat( conf->path, &st ) || !S_ISDIR(st.st_mode)) { + fprintf( stderr, "Maildir error: cannot open store %s\n", conf->path ); + return 0; + } + ctx = nfcalloc( sizeof(*ctx) ); + ctx->gen.conf = conf; + ctx->uvfd = -1; + return &ctx->gen; +} + +static void +free_maildir_messages( message_t *msg ) +{ + message_t *tmsg; + + for (; (tmsg = msg); msg = tmsg) { + tmsg = msg->next; + free( ((maildir_message_t *)msg)->base ); + free( msg ); + } +} + +static void +maildir_cleanup( store_t *gctx ) +{ + maildir_store_t *ctx = (maildir_store_t *)gctx; + + free_maildir_messages( gctx->msgs ); +#ifdef USE_DB + if (ctx->db) + ctx->db->close( ctx->db, 0 ); +#endif /* USE_DB */ + if (gctx->path) + free( gctx->path ); + if (ctx->excs) + free( ctx->excs ); + if (ctx->uvfd >= 0) + close( ctx->uvfd ); +} + +static void +maildir_close_store( store_t *gctx ) +{ + maildir_cleanup( gctx ); + free( gctx ); +} + +static int +maildir_list( store_t *gctx, string_list_t **retb ) +{ + DIR *dir; + struct dirent *de; + + if (!(dir = opendir( gctx->conf->path ))) { + fprintf( stderr, "%s: %s\n", gctx->conf->path, strerror(errno) ); + return DRV_STORE_BAD; + } + *retb = 0; + while ((de = readdir( dir ))) { + struct stat st; + char buf[PATH_MAX]; + + if (*de->d_name == '.') + continue; + nfsnprintf( buf, sizeof(buf), "%s%s/cur", gctx->conf->path, de->d_name ); + if (stat( buf, &st ) || !S_ISDIR(st.st_mode)) + continue; + add_string_list( retb, de->d_name ); + } + closedir (dir); + + return DRV_OK; +} + +static const char *subdirs[] = { "cur", "new", "tmp" }; + +typedef struct { + char *base; + int size; + unsigned uid:31, recent:1; +} msg_t; + +typedef struct { + msg_t *ents; + int nents, nalloc; +} msglist_t; + +static void +maildir_free_scan( msglist_t *msglist ) +{ + int i; + + if (msglist->ents) { + for (i = 0; i < msglist->nents; i++) + if (msglist->ents[i].base) + free( msglist->ents[i].base ); + free( msglist->ents ); + } +} + +#define _24_HOURS (3600 * 24) + +static int +maildir_validate( const char *prefix, const char *box, int create ) +{ + DIR *dirp; + struct dirent *entry; + time_t now; + int i, bl; + struct stat st; + char buf[_POSIX_PATH_MAX]; + + bl = nfsnprintf( buf, sizeof(buf) - 4, "%s%s/", prefix, box ); + if (stat( buf, &st )) { + if (errno == ENOENT) { + if (create) { + if (mkdir( buf, 0700 )) { + fprintf( stderr, "Maildir error: mkdir %s: %s (errno %d)\n", + buf, strerror(errno), errno ); + return DRV_STORE_BAD; + } + for (i = 0; i < 3; i++) { + memcpy( buf + bl, subdirs[i], 4 ); + if (mkdir( buf, 0700 )) { + fprintf( stderr, "Maildir error: mkdir %s: %s (errno %d)\n", + buf, strerror(errno), errno ); + return DRV_BOX_BAD; + } + } + } else { + fprintf( stderr, "Maildir error: mailbox '%s' does not exist\n", buf ); + return DRV_BOX_BAD; + } + } else { + fprintf( stderr, "Maildir error: stat %s: %s (errno %d)\n", + buf, strerror(errno), errno ); + return DRV_BOX_BAD; + } + } else { + for (i = 0; i < 3; i++) { + memcpy( buf + bl, subdirs[i], 4 ); + if (stat( buf, &st ) || !S_ISDIR(st.st_mode)) { + fprintf( stderr, "Maildir error: '%s' is no valid mailbox\n", buf ); + return DRV_BOX_BAD; + } + } + memcpy( buf + bl, "tmp/", 5 ); + bl += 4; + if (!(dirp = opendir( buf ))) { + fprintf( stderr, "Maildir error: opendir: %s: %s (errno %d)\n", + buf, strerror(errno), errno ); + return DRV_BOX_BAD; + } + time( &now ); + while ((entry = readdir( dirp ))) { + nfsnprintf( buf + bl, sizeof(buf) - bl, "%s", entry->d_name ); + if (stat( buf, &st )) + fprintf( stderr, "Maildir error: stat: %s: %s (errno %d)\n", + buf, strerror(errno), errno ); + else if (S_ISREG(st.st_mode) && now - st.st_ctime >= _24_HOURS) { + /* this should happen infrequently enough that it won't be + * bothersome to the user to display when it occurs. + */ + info( "Maildir notice: removing stale file %s\n", buf ); + if (unlink( buf )) + fprintf( stderr, "Maildir error: unlink: %s: %s (errno %d)\n", + buf, strerror(errno), errno ); + } + } + closedir( dirp ); + } + return DRV_OK; +} + +static int +maildir_uidval_lock( maildir_store_t *ctx ) +{ + int n; + char buf[128]; + + /* This is legacy only */ + if (flock( ctx->uvfd, LOCK_EX ) < 0) { + fprintf( stderr, "Maildir error: cannot flock UIDVALIDITY.\n" ); + return DRV_BOX_BAD; + } + /* This (theoretically) works over NFS. Let's hope nobody else did + the same in the opposite order, as we'd deadlock then. */ +#if SEEK_SET != 0 + lck.l_whence = SEEK_SET; +#endif + lck.l_type = F_WRLCK; + if (fcntl( ctx->uvfd, F_SETLKW, &lck )) { + fprintf( stderr, "Maildir error: cannot fcntl lock UIDVALIDITY.\n" ); + return DRV_BOX_BAD; + } + lseek( ctx->uvfd, 0, SEEK_SET ); + if ((n = read( ctx->uvfd, buf, sizeof(buf) )) <= 0 || + (buf[n] = 0, sscanf( buf, "%d\n%d", &ctx->gen.uidvalidity, &ctx->nuid ) != 2)) { + info( "Maildir notice: cannot read UIDVALIDITY, creating new.\n" ); + ctx->gen.uidvalidity = time( 0 ); + ctx->nuid = 0; + ctx->uvok = 0; + } else + ctx->uvok = 1; + return DRV_OK; +} + +static void +maildir_uidval_unlock( maildir_store_t *ctx ) +{ + lck.l_type = F_UNLCK; + fcntl( ctx->uvfd, F_SETLK, &lck ); + /* This is legacy only */ + flock( ctx->uvfd, LOCK_UN ); +} + +static int +maildir_obtain_uid( maildir_store_t *ctx, int *uid ) +{ + int n; + char buf[128]; + + *uid = ++ctx->nuid; + n = sprintf( buf, "%d\n%d\n", ctx->gen.uidvalidity, ctx->nuid ); + lseek( ctx->uvfd, 0, SEEK_SET ); + if (write( ctx->uvfd, buf, n ) != n || ftruncate( ctx->uvfd, n )) { + fprintf( stderr, "Maildir error: cannot write UIDVALIDITY.\n" ); + return DRV_BOX_BAD; + } + return DRV_OK; +} + +#ifdef USE_DB +static void +make_key( DBT *tkey, char *name ) +{ + char *u = strpbrk( name, ":," ); + tkey->data = name; + tkey->size = u ? (size_t)(u - name) : strlen( name ); +} + +static int +maildir_set_uid( maildir_store_t *ctx, const char *name, int *uid ) +{ + int ret, uv[2]; + + *uid = ++ctx->nuid; + key.data = (void *)"UIDVALIDITY"; + key.size = 11; + uv[0] = ctx->gen.uidvalidity; + uv[1] = ctx->nuid; + value.data = uv; + value.size = sizeof(uv); + if ((ret = ctx->db->put( ctx->db, 0, &key, &value, 0 ))) { + tbork: + ctx->db->err( ctx->db, ret, "Maildir error: db->put()" ); + bork: + ctx->db->close( ctx->db, 0 ); + ctx->db = 0; + return DRV_BOX_BAD; + } + make_key( &key, (char *)name ); + value.data = uid; + value.size = sizeof(*uid); + if ((ret = ctx->db->put( ctx->db, 0, &key, &value, 0 ))) + goto tbork; + if ((ret = ctx->db->sync( ctx->db, 0 ))) { + ctx->db->err( ctx->db, ret, "Maildir error: db->sync()" ); + goto bork; + } + return DRV_OK; +} +#endif /* USE_DB */ + +static int +maildir_compare( const void *l, const void *r ) +{ + msg_t *lm = (msg_t *)l, *rm = (msg_t *)r; + char *ldot, *rdot, *ldot2, *rdot2, *lseq, *rseq; + int ret, llen, rlen; + + if ((ret = lm->uid - rm->uid)) + return ret; + + /* No UID, so sort by arrival date. We should not do this, but we rely + on the suggested unique file name scheme - we have no choice. */ + /* The first field are always the seconds. Alphabetical sort should be + faster than numeric. */ + if (!(ldot = strchr( lm->base, '.' )) || !(rdot = strchr( rm->base, '.' ))) + goto stronly; /* Should never happen ... */ + llen = ldot - lm->base, rlen = rdot - rm->base; + /* The shorter number is smaller. Really. This won't trigger with any + mail created after Sep 9 2001 anyway. */ + if ((ret = llen - rlen)) + return ret; + if ((ret = memcmp( lm->base, rm->base, llen ))) + return ret; + + ldot++, rdot++; + + if ((llen = strtol( ldot, &ldot2, 10 ))) { + if (!(rlen = strtol( rdot, &rdot2, 10 ))) + goto stronly; /* Comparing apples to oranges ... */ + /* Classical PID specs */ + if ((ret = llen - rlen)) { + retpid: + /* Handle PID wraparound. This works only on systems + where PIDs are not reused too fast */ + if (ret > 20000 || ret < -20000) + ret = -ret; + return ret; + } + return (*ldot2 != '_' ? 0 : atoi( ldot2 + 1 )) - + (*rdot2 != '_' ? 0 : atoi( rdot2 + 1 )); + } + + if (!(ldot2 = strchr( ldot, '.' )) || !(rdot2 = strchr( rdot, '.' ))) + goto stronly; /* Should never happen ... */ + llen = ldot2 - ldot, rlen = rdot2 - rdot; + + if (((lseq = memchr( ldot, '#', llen )) && (rseq = memchr( rdot, '#', rlen ))) || + ((lseq = memchr( ldot, 'M', llen )) && (rseq = memchr( rdot, 'M', rlen )))) + return atoi( lseq + 1 ) - atoi( rseq + 1 ); + + if ((lseq = memchr( ldot, 'P', llen )) && (rseq = memchr( rdot, 'P', rlen ))) { + if ((ret = atoi( lseq + 1 ) - atoi( rseq + 1 ))) + goto retpid; + if ((lseq = memchr( ldot, 'Q', llen )) && (rseq = memchr( rdot, 'Q', rlen ))) + return atoi( lseq + 1 ) - atoi( rseq + 1 ); + } + + stronly: + /* Fall-back, so the sort order is defined at all */ + return strcmp( lm->base, rm->base ); +} + +static int +maildir_scan( maildir_store_t *ctx, msglist_t *msglist ) +{ + DIR *d; + struct dirent *e; + const char *u, *ru; +#ifdef USE_DB + DB *tdb; + DBC *dbc; +#endif /* USE_DB */ + msg_t *entry; + int i, j, uid, bl, ml, fnl, ret; + struct stat st; + char buf[_POSIX_PATH_MAX], nbuf[_POSIX_PATH_MAX]; + +#ifdef USE_DB + if (!ctx->db) +#endif /* USE_DB */ + if ((ret = maildir_uidval_lock( ctx )) != DRV_OK) + return ret; + + again: + msglist->ents = 0; + msglist->nents = msglist->nalloc = 0; + ctx->gen.count = ctx->gen.recent = 0; + if (ctx->uvok || ctx->maxuid == INT_MAX) { +#ifdef USE_DB + if (ctx->db) { + if (db_create( &tdb, 0, 0 )) { + fputs( "Maildir error: db_create() failed\n", stderr ); + return DRV_BOX_BAD; + } + if (tdb->open( tdb, 0, 0, 0, DB_HASH, DB_CREATE, 0 )) { + fputs( "Maildir error: tdb->open() failed\n", stderr ); + tdb->close( tdb, 0 ); + return DRV_BOX_BAD; + } + } +#endif /* USE_DB */ + bl = nfsnprintf( buf, sizeof(buf) - 4, "%s/", ctx->gen.path ); + for (i = 0; i < 2; i++) { + memcpy( buf + bl, subdirs[i], 4 ); + if (!(d = opendir( buf ))) { + perror( buf ); +#ifdef USE_DB + if (!ctx->db) +#endif /* USE_DB */ + maildir_uidval_unlock( ctx ); +#ifdef USE_DB + bork: +#endif /* USE_DB */ + maildir_free_scan( msglist ); +#ifdef USE_DB + if (ctx->db) + tdb->close( tdb, 0 ); +#endif /* USE_DB */ + return DRV_BOX_BAD; + } + while ((e = readdir( d ))) { + if (*e->d_name == '.') + continue; + ctx->gen.count++; + ctx->gen.recent += i; +#ifdef USE_DB + if (ctx->db) { + make_key( &key, e->d_name ); + if ((ret = ctx->db->get( ctx->db, 0, &key, &value, 0 ))) { + if (ret != DB_NOTFOUND) { + ctx->db->err( ctx->db, ret, "Maildir error: db->get()" ); + ctx->db->close( ctx->db, 0 ); + ctx->db = 0; + goto bork; + } + uid = INT_MAX; + } else { + value.size = 0; + if ((ret = tdb->put( tdb, 0, &key, &value, 0 ))) { + tdb->err( tdb, ret, "Maildir error: tdb->put()" ); + goto bork; + } + uid = *(int *)value.data; + } + } else +#endif /* USE_DB */ + { + uid = (ctx->uvok && (u = strstr( e->d_name, ",U=" ))) ? atoi( u + 3 ) : 0; + if (!uid) + uid = INT_MAX; + } + if (uid <= ctx->maxuid) { + if (uid < ctx->minuid) { + for (j = 0; j < ctx->nexcs; j++) + if (ctx->excs[j] == uid) + goto oke; + continue; + oke: ; + } + if (msglist->nalloc == msglist->nents) { + msglist->nalloc = msglist->nalloc * 2 + 100; + msglist->ents = nfrealloc( msglist->ents, msglist->nalloc * sizeof(msg_t) ); + } + entry = &msglist->ents[msglist->nents++]; + entry->base = nfstrdup( e->d_name ); + entry->uid = uid; + entry->recent = i; + entry->size = 0; + } + } + closedir( d ); + } +#ifdef USE_DB + if (ctx->db) { + if ((ret = ctx->db->cursor( ctx->db, 0, &dbc, 0 ))) + ctx->db->err( ctx->db, ret, "Maildir error: db->cursor()" ); + else { + for (;;) { + if ((ret = dbc->c_get( dbc, &key, &value, DB_NEXT ))) { + if (ret != DB_NOTFOUND) + ctx->db->err( ctx->db, ret, "Maildir error: db->c_get()" ); + break; + } + if ((key.size != 11 || memcmp( key.data, "UIDVALIDITY", 11 )) && + (ret = tdb->get( tdb, 0, &key, &value, 0 ))) { + if (ret != DB_NOTFOUND) { + tdb->err( tdb, ret, "Maildir error: tdb->get()" ); + break; + } + if ((ret = dbc->c_del( dbc, 0 ))) { + ctx->db->err( ctx->db, ret, "Maildir error: db->c_del()" ); + break; + } + } + } + dbc->c_close( dbc ); + } + tdb->close( tdb, 0 ); + } +#endif /* USE_DB */ + qsort( msglist->ents, msglist->nents, sizeof(msg_t), maildir_compare ); + for (uid = i = 0; i < msglist->nents; i++) { + entry = &msglist->ents[i]; + if (entry->uid != INT_MAX) { + if (uid == entry->uid) { + info( "Maildir notice: duplicate UID; changing UIDVALIDITY.\n" ); + ctx->gen.uidvalidity = time( 0 ); + ctx->nuid = 0; + ctx->uvok = 0; +#ifdef USE_DB + if (ctx->db) + ctx->db->truncate( ctx->db, 0, 0 /* &u_int32_t_dummy */, 0 ); +#endif /* USE_DB */ + maildir_free_scan( msglist ); + goto again; + } + uid = entry->uid; + if (ctx->gen.opts & OPEN_SIZE) + nfsnprintf( buf + bl, sizeof(buf) - bl, "%s/%s", subdirs[entry->recent], entry->base ); +#ifdef USE_DB + } else if (ctx->db) { + if ((ret = maildir_set_uid( ctx, entry->base, &uid )) != DRV_OK) { + maildir_free_scan( msglist ); + return ret; + } + entry->uid = uid; + if (ctx->gen.opts & OPEN_SIZE) + nfsnprintf( buf + bl, sizeof(buf) - bl, "%s/%s", subdirs[entry->recent], entry->base ); +#endif /* USE_DB */ + } else { + if ((ret = maildir_obtain_uid( ctx, &uid )) != DRV_OK) { + maildir_free_scan( msglist ); + return ret; + } + entry->uid = uid; + if ((u = strstr( entry->base, ",U=" ))) + for (ru = u + 3; isdigit( (unsigned char)*ru ); ru++); + else + u = ru = strchr( entry->base, ':' ); + if (u) + ml = u - entry->base; + else + ru = "", ml = INT_MAX; + fnl = nfsnprintf( buf + bl, sizeof(buf) - bl, "%s/%.*s,U=%d%s", subdirs[entry->recent], ml, entry->base, uid, ru ) + 1 - 4; + memcpy( nbuf, buf, bl + 4 ); + nfsnprintf( nbuf + bl + 4, sizeof(nbuf) - bl - 4, "%s", entry->base ); + if (rename( nbuf, buf )) { + if (errno != ENOENT) { + perror( buf ); + maildir_uidval_unlock( ctx ); + maildir_free_scan( msglist ); + return DRV_BOX_BAD; + } + maildir_free_scan( msglist ); + goto again; + } + free( entry->base ); + entry->base = nfmalloc( fnl ); + memcpy( entry->base, buf + bl + 4, fnl ); + } + if (ctx->gen.opts & OPEN_SIZE) { + if (stat( buf, &st )) { + maildir_free_scan( msglist ); + goto again; + } + entry->size = st.st_size; + } + } + } +#ifdef USE_DB + if (!ctx->db) +#endif /* ! USE_DB */ + maildir_uidval_unlock( ctx ); + return DRV_OK; +} + +static void +maildir_init_msg( maildir_store_t *ctx, maildir_message_t *msg, msg_t *entry ) +{ + msg->base = entry->base; + entry->base = 0; /* prevent deletion */ + msg->gen.size = entry->size; + if (entry->recent) + msg->gen.status |= M_RECENT; + if (ctx->gen.opts & OPEN_FLAGS) { + msg->gen.status |= M_FLAGS; + msg->gen.flags = maildir_parse_flags( msg->base ); + } else + msg->gen.flags = 0; +} + +static void +maildir_app_msg( maildir_store_t *ctx, message_t ***msgapp, msg_t *entry ) +{ + maildir_message_t *msg = nfmalloc( sizeof(*msg) ); + msg->gen.next = **msgapp; + **msgapp = &msg->gen; + *msgapp = &msg->gen.next; + msg->gen.uid = entry->uid; + msg->gen.status = 0; + maildir_init_msg( ctx, msg, entry ); +} + +static void +maildir_prepare( store_t *gctx, int opts ) +{ + maildir_store_t *ctx = (maildir_store_t *)gctx; + + maildir_cleanup( gctx ); + gctx->msgs = 0; + ctx->uvfd = -1; +#ifdef USE_DB + ctx->db = 0; +#endif /* USE_DB */ + if (!strcmp( gctx->name, "INBOX" )) + gctx->path = nfstrdup( ((maildir_store_conf_t *)gctx->conf)->inbox ); + else + nfasprintf( &gctx->path, "%s%s", gctx->conf->path, gctx->name ); + if (opts & OPEN_SETFLAGS) + opts |= OPEN_OLD; + if (opts & OPEN_EXPUNGE) + opts |= OPEN_OLD|OPEN_NEW|OPEN_FLAGS; + gctx->opts = opts; +} + +static int +maildir_select( store_t *gctx, int minuid, int maxuid, int *excs, int nexcs ) +{ + maildir_store_t *ctx = (maildir_store_t *)gctx; + message_t **msgapp; + msglist_t msglist; + int i; +#ifdef USE_DB + int ret; +#endif /* USE_DB */ + char uvpath[_POSIX_PATH_MAX]; + + ctx->minuid = minuid; + ctx->maxuid = maxuid; + ctx->excs = nfrealloc( excs, nexcs * sizeof(int) ); + ctx->nexcs = nexcs; + + if (maildir_validate( gctx->path, "", ctx->gen.opts & OPEN_CREATE ) != DRV_OK) + return DRV_BOX_BAD; + + nfsnprintf( uvpath, sizeof(uvpath), "%s/.uidvalidity", gctx->path ); +#ifndef USE_DB + if ((ctx->uvfd = open( uvpath, O_RDWR|O_CREAT, 0600 )) < 0) { + perror( uvpath ); + return DRV_BOX_BAD; + } +#else + if ((ctx->uvfd = open( uvpath, O_RDWR, 0600 )) < 0) { + nfsnprintf( uvpath, sizeof(uvpath), "%s/.isyncuidmap.db", gctx->path ); + if ((ctx->uvfd = open( uvpath, O_RDWR, 0600 )) < 0) { + if (((maildir_store_conf_t *)gctx->conf)->alt_map) { + if ((ctx->uvfd = open( uvpath, O_RDWR|O_CREAT, 0600 )) >= 0) + goto dbok; + } else { + nfsnprintf( uvpath, sizeof(uvpath), "%s/.uidvalidity", gctx->path ); + if ((ctx->uvfd = open( uvpath, O_RDWR|O_CREAT, 0600 )) >= 0) + goto fnok; + } + perror( uvpath ); + return DRV_BOX_BAD; + } + dbok: +#if SEEK_SET != 0 + lck.l_whence = SEEK_SET; +#endif + lck.l_type = F_WRLCK; + if (fcntl( ctx->uvfd, F_SETLKW, &lck )) { + perror( uvpath ); + bork: + close( ctx->uvfd ); + ctx->uvfd = -1; + return DRV_BOX_BAD; + } + if (db_create( &ctx->db, 0, 0 )) { + fputs( "Maildir error: db_create() failed\n", stderr ); + goto bork; + } + if ((ret = ctx->db->open( ctx->db, 0, uvpath, 0, DB_HASH, DB_CREATE, 0 ))) { + ctx->db->err( ctx->db, ret, "Maildir error: db->open(%s)", uvpath ); + dbork: + ctx->db->close( ctx->db, 0 ); + goto bork; + } + key.data = (void *)"UIDVALIDITY"; + key.size = 11; + if ((ret = ctx->db->get( ctx->db, 0, &key, &value, 0 ))) { + if (ret != DB_NOTFOUND) { + ctx->db->err( ctx->db, ret, "Maildir error: db->get()" ); + goto dbork; + } + info( "Maildir notice: cannot read UIDVALIDITY, creating new.\n" ); + ctx->gen.uidvalidity = time( 0 ); + ctx->nuid = 0; + ctx->uvok = 0; + } else { + ctx->gen.uidvalidity = ((int *)value.data)[0]; + ctx->nuid = ((int *)value.data)[1]; + ctx->uvok = 1; + } + } + fnok: +#endif /* USE_DB */ + + if (maildir_scan( ctx, &msglist ) != DRV_OK) + return DRV_BOX_BAD; + msgapp = &ctx->gen.msgs; + for (i = 0; i < msglist.nents; i++) + maildir_app_msg( ctx, &msgapp, msglist.ents + i ); + maildir_free_scan( &msglist ); + + return DRV_OK; +} + +static int +maildir_rescan( maildir_store_t *ctx ) +{ + message_t **msgapp; + maildir_message_t *msg; + msglist_t msglist; + int i; + + if (maildir_scan( ctx, &msglist ) != DRV_OK) + return DRV_BOX_BAD; + ctx->gen.recent = 0; + for (msgapp = &ctx->gen.msgs, i = 0; + (msg = (maildir_message_t *)*msgapp) || i < msglist.nents; ) + { + if (!msg || msglist.ents[i].uid < msg->gen.uid) { + debug( "ignoring new message %d\n", msglist.ents[i].uid ); + /* maildir_app_msg( ctx, &msgapp, msglist.ents + i ); */ + i++; + } else if (i >= msglist.nents || msglist.ents[i].uid > msg->gen.uid) { + debug( "purging deleted message %d\n", msg->gen.uid ); + msg->gen.status = M_DEAD; + msgapp = &msg->gen.next; + } else { + debug( "updating message %d\n", msg->gen.uid ); + msg->gen.status &= ~(M_FLAGS|M_RECENT); + free( msg->base ); + maildir_init_msg( ctx, msg, msglist.ents + i ); + i++, msgapp = &msg->gen.next; + } + } + maildir_free_scan( &msglist ); + return DRV_OK; +} + +static int +maildir_again( maildir_store_t *ctx, maildir_message_t *msg, const char *fn ) +{ + int ret; + + if (errno != ENOENT) { + perror( fn ); + return DRV_BOX_BAD; + } + if ((ret = maildir_rescan( ctx )) != DRV_OK) + return ret; + return (msg->gen.status & M_DEAD) ? DRV_OK : DRV_MSG_BAD; +} + +static int +maildir_fetch_msg( store_t *gctx, message_t *gmsg, msg_data_t *data ) +{ + maildir_store_t *ctx = (maildir_store_t *)gctx; + maildir_message_t *msg = (maildir_message_t *)gmsg; + int fd, ret; + struct stat st; + char buf[_POSIX_PATH_MAX]; + + for (;;) { + nfsnprintf( buf, sizeof(buf), "%s/%s/%s", gctx->path, subdirs[gmsg->status & M_RECENT], msg->base ); + if ((fd = open( buf, O_RDONLY )) >= 0) + break; + if ((ret = maildir_again( ctx, msg, buf )) != DRV_OK) + return ret; + } + fstat( fd, &st ); + data->crlf = 0; + data->len = st.st_size; + data->data = nfmalloc( data->len ); + if (read( fd, data->data, data->len ) != data->len) { + perror( buf ); + close( fd ); + return DRV_MSG_BAD; + } + close( fd ); + if (!(gmsg->status & M_FLAGS)) + data->flags = maildir_parse_flags( msg->base ); + return DRV_OK; +} + +static int +maildir_make_flags( int flags, char *buf ) +{ + unsigned i, d; + + buf[0] = ':'; + buf[1] = '2'; + buf[2] = ','; + for (d = 3, i = 0; i < as(Flags); i++) + if (flags & (1 << i)) + buf[d++] = Flags[i]; + buf[d] = 0; + return d; +} + +static int +maildir_store_msg( store_t *gctx, msg_data_t *data, int *uid ) +{ + maildir_store_t *ctx = (maildir_store_t *)gctx; + const char *prefix, *box; + int ret, fd, bl; + char buf[_POSIX_PATH_MAX], nbuf[_POSIX_PATH_MAX], fbuf[NUM_FLAGS + 3], base[128]; + + bl = nfsnprintf( base, sizeof(base), "%ld.%d_%d.%s", time( 0 ), Pid, ++MaildirCount, Hostname ); + if (uid) { +#ifdef USE_DB + if (ctx->db) { + if ((ret = maildir_set_uid( ctx, base, uid )) != DRV_OK) { + free( data->data ); + return ret; + } + } else +#endif /* USE_DB */ + { + if ((ret = maildir_uidval_lock( ctx )) != DRV_OK || + (ret = maildir_obtain_uid( ctx, uid )) != DRV_OK) + return ret; + maildir_uidval_unlock( ctx ); + nfsnprintf( base + bl, sizeof(base) - bl, ",U=%d", *uid ); + } + prefix = gctx->path; + box = ""; + } else { + prefix = gctx->conf->path; + box = gctx->conf->trash; + } + + maildir_make_flags( data->flags, fbuf ); + nfsnprintf( buf, sizeof(buf), "%s%s/tmp/%s%s", prefix, box, base, fbuf ); + if ((fd = open( buf, O_WRONLY|O_CREAT|O_EXCL, 0600 )) < 0) { + if (errno != ENOENT) { + perror( buf ); + free( data->data ); + return DRV_BOX_BAD; + } + if ((ret = maildir_validate( gctx->conf->path, gctx->conf->trash, gctx->opts & OPEN_CREATE )) != DRV_OK) { + free( data->data ); + return ret; + } + if ((fd = open( buf, O_WRONLY|O_CREAT|O_EXCL, 0600 )) < 0) { + perror( buf ); + free( data->data ); + return DRV_BOX_BAD; + } + } + strip_cr( data ); + ret = write( fd, data->data, data->len ); + free( data->data ); + if (ret != data->len) { + if (ret < 0) + perror( buf ); + else + fprintf( stderr, "Maildir error: %s: partial write\n", buf ); + close( fd ); + return DRV_BOX_BAD; + } + close( fd ); + nfsnprintf( nbuf, sizeof(nbuf), "%s%s/new/%s%s", prefix, box, base, fbuf ); + if (rename( buf, nbuf )) { + perror( nbuf ); + return DRV_BOX_BAD; + } + if (uid) + gctx->count++; + return DRV_OK; +} + +static int +maildir_set_flags( store_t *gctx, message_t *gmsg, int uid, int add, int del ) +{ + maildir_store_t *ctx = (maildir_store_t *)gctx; + maildir_message_t *msg = (maildir_message_t *)gmsg; + char *s, *p; + unsigned i; + int j, ret, ol, fl, bbl, bl, tl; + char buf[_POSIX_PATH_MAX], nbuf[_POSIX_PATH_MAX]; + + (void) uid; + bbl = nfsnprintf( buf, sizeof(buf), "%s/", gctx->path ); + for (;;) { + bl = bbl + nfsnprintf( buf + bbl, sizeof(buf) - bbl, "%s/", subdirs[gmsg->status & M_RECENT] ); + ol = strlen( msg->base ); + if ((int)sizeof(buf) - bl < ol + 3 + NUM_FLAGS) + oob(); + memcpy( buf + bl, msg->base, ol + 1 ); + memcpy( nbuf, buf, bl + ol + 1 ); + if ((s = strstr( nbuf + bl, ":2," ))) { + s += 3; + fl = ol - (s - (nbuf + bl)); + for (i = 0; i < as(Flags); i++) { + if ((p = strchr( s, Flags[i] ))) { + if (del & (1 << i)) { + memcpy( p, p + 1, fl - (p - s) ); + fl--; + } + } else if (add & (1 << i)) { + for (j = 0; j < fl && Flags[i] > s[j]; j++); + fl++; + memmove( s + j + 1, s + j, fl - j ); + s[j] = Flags[i]; + } + } + tl = ol + 3 + fl; + } else { + tl = ol + maildir_make_flags( msg->gen.flags, nbuf + bl + ol ); + } + if (!rename( buf, nbuf )) + break; + if ((ret = maildir_again( ctx, msg, buf )) != DRV_OK) + return ret; + } + free( msg->base ); + msg->base = nfmalloc( tl + 1 ); + memcpy( msg->base, nbuf + bl, tl + 1 ); + msg->gen.flags |= add; + msg->gen.flags &= ~del; + + return DRV_OK; +} + +#ifdef USE_DB +static int +maildir_purge_msg( maildir_store_t *ctx, const char *name ) +{ + int ret; + + make_key( &key, (char *)name ); + if ((ret = ctx->db->del( ctx->db, 0, &key, 0 ))) { + ctx->db->err( ctx->db, ret, "Maildir error: db->del()" ); + ctx->db->close( ctx->db, 0 ); + ctx->db = 0; + return DRV_BOX_BAD; + } + return DRV_OK; +} +#endif /* USE_DB */ + +static int +maildir_trash_msg( store_t *gctx, message_t *gmsg ) +{ + maildir_store_t *ctx = (maildir_store_t *)gctx; + maildir_message_t *msg = (maildir_message_t *)gmsg; + char *s; + int ret; + struct stat st; + char buf[_POSIX_PATH_MAX], nbuf[_POSIX_PATH_MAX]; + + for (;;) { + nfsnprintf( buf, sizeof(buf), "%s/%s/%s", gctx->path, subdirs[gmsg->status & M_RECENT], msg->base ); + s = strstr( msg->base, ":2," ); + nfsnprintf( nbuf, sizeof(nbuf), "%s%s/new/%ld.%d_%d.%s%s", gctx->conf->path, gctx->conf->trash, + time( 0 ), Pid, ++MaildirCount, Hostname, s ? s : "" ); + if (!rename( buf, nbuf )) + break; + if (!stat( buf, &st )) { + if ((ret = maildir_validate( gctx->conf->path, gctx->conf->trash, 1 )) != DRV_OK) + return ret; + if (!rename( buf, nbuf )) + break; + perror( nbuf ); + return DRV_BOX_BAD; + } + if ((ret = maildir_again( ctx, msg, buf )) != DRV_OK) + return ret; + } + gmsg->status |= M_DEAD; + gctx->count--; + +#ifdef USE_DB + if (ctx->db) + return maildir_purge_msg( ctx, msg->base ); +#endif /* USE_DB */ + return DRV_OK; +} + +static int +maildir_close( store_t *gctx ) +{ +#ifdef USE_DB + maildir_store_t *ctx = (maildir_store_t *)gctx; +#endif /* USE_DB */ + message_t *msg; + int basel, retry, ret; + char buf[_POSIX_PATH_MAX]; + + for (;;) { + retry = 0; + basel = nfsnprintf( buf, sizeof(buf), "%s/", gctx->path ); + for (msg = gctx->msgs; msg; msg = msg->next) + if (!(msg->status & M_DEAD) && (msg->flags & F_DELETED)) { + nfsnprintf( buf + basel, sizeof(buf) - basel, "%s/%s", subdirs[msg->status & M_RECENT], ((maildir_message_t *)msg)->base ); + if (unlink( buf )) { + if (errno == ENOENT) + retry = 1; + else + perror( buf ); + } else { + msg->status |= M_DEAD; + gctx->count--; +#ifdef USE_DB + if (ctx->db && (ret = maildir_purge_msg( ctx, ((maildir_message_t *)msg)->base )) != DRV_OK) + return ret; +#endif /* USE_DB */ + } + } + if (!retry) + return DRV_OK; + if ((ret = maildir_rescan( (maildir_store_t *)gctx )) != DRV_OK) + return ret; + } +} + +static int +maildir_check( store_t *gctx ) +{ + (void) gctx; + return DRV_OK; +} + +static int +maildir_parse_store( conffile_t *cfg, store_conf_t **storep, int *err ) +{ + maildir_store_conf_t *store; + + if (strcasecmp( "MaildirStore", cfg->cmd )) + return 0; + store = nfcalloc( sizeof(*store) ); + store->gen.driver = &maildir_driver; + store->gen.name = nfstrdup( cfg->val ); + + while (getcline( cfg ) && cfg->cmd) + if (!strcasecmp( "Inbox", cfg->cmd )) + store->inbox = expand_strdup( cfg->val ); + else if (!strcasecmp( "Path", cfg->cmd )) + store->gen.path = expand_strdup( cfg->val ); +#ifdef USE_DB + else if (!strcasecmp( "AltMap", cfg->cmd )) + store->alt_map = parse_bool( cfg ); +#endif /* USE_DB */ + else + parse_generic_store( &store->gen, cfg, err ); + if (!store->inbox) + store->inbox = expand_strdup( "~/Maildir" ); + *storep = &store->gen; + return 1; +} + +struct driver maildir_driver = { + maildir_parse_store, + maildir_open_store, + maildir_close_store, + maildir_list, + maildir_prepare, + maildir_select, + maildir_fetch_msg, + maildir_store_msg, + maildir_set_flags, + maildir_trash_msg, + maildir_check, + maildir_close +}; diff --git a/src/imap.c b/src/imap.c @@ -1,1413 +0,0 @@ -/* $Id$ - * - * isync - IMAP4 to maildir mailbox synchronizer - * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> - * Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net> - * - * 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 - * - * As a special exception, isync may be linked with the OpenSSL library, - * despite that library's more restrictive license. - */ - -#include "isync.h" - -#include <assert.h> -#include <unistd.h> -#include <sys/mman.h> -#include <sys/time.h> -#include <stdlib.h> -#include <stdio.h> -#include <stdarg.h> -#include <errno.h> -#include <string.h> -#include <ctype.h> -#include <sys/socket.h> -#include <netinet/in.h> -#include <arpa/inet.h> -#include <netdb.h> -#if HAVE_LIBSSL -# include <openssl/err.h> -#endif - -#define as(ar) (sizeof(ar)/sizeof(ar[0])) - -#define CAP(cap) (imap->caps & (1 << (cap))) - -static int Tag; - -const char *Flags[] = { - "\\Seen", - "\\Answered", - "\\Deleted", - "\\Flagged", - "\\Recent", - "\\Draft" -}; - -void -free_message (message_t * msg) -{ - message_t *tmp; - - while (msg) - { - tmp = msg; - msg = msg->next; - if (tmp->file) - free (tmp->file); - free (tmp); - } -} - -#if HAVE_LIBSSL - -#define MAX_DEPTH 1 - -SSL_CTX *SSLContext = 0; - -/* this gets called when a certificate is to be verified */ -static int -verify_cert (SSL * ssl) -{ - X509 *cert; - int err; - char buf[256]; - int ret = -1; - BIO *bio; - - cert = SSL_get_peer_certificate (ssl); - if (!cert) - { - fprintf (stderr, "Error, no server certificate\n"); - return -1; - } - - err = SSL_get_verify_result (ssl); - if (err == X509_V_OK) - return 0; - - fprintf (stderr, "Error, can't verify certificate: %s (%d)\n", - X509_verify_cert_error_string (err), err); - - X509_NAME_oneline (X509_get_subject_name (cert), buf, sizeof (buf)); - info ("\nSubject: %s\n", buf); - X509_NAME_oneline (X509_get_issuer_name (cert), buf, sizeof (buf)); - info ("Issuer: %s\n", buf); - bio = BIO_new (BIO_s_mem ()); - ASN1_TIME_print (bio, X509_get_notBefore (cert)); - memset (buf, 0, sizeof (buf)); - BIO_read (bio, buf, sizeof (buf) - 1); - info ("Valid from: %s\n", buf); - ASN1_TIME_print (bio, X509_get_notAfter (cert)); - memset (buf, 0, sizeof (buf)); - BIO_read (bio, buf, sizeof (buf) - 1); - BIO_free (bio); - info (" to: %s\n", buf); - - fprintf (stderr, - "\n*** WARNING *** There is no way to verify this certificate. It is\n" - " possible that a hostile attacker has replaced the\n" - " server certificate. Continue at your own risk!\n" - "\nAccept this certificate anyway? [no]: "); - if (fgets (buf, sizeof (buf), stdin) && (buf[0] == 'y' || buf[0] == 'Y')) - { - ret = 0; - fprintf (stderr, "\n*** Fine, but don't say I didn't warn you!\n\n"); - } - return ret; -} - -static int -init_ssl (config_t * conf) -{ - SSL_METHOD *method; - int options = 0; - - if (!conf->cert_file) - { - fprintf (stderr, "Error, CertificateFile not defined\n"); - return -1; - } - SSL_library_init (); - SSL_load_error_strings (); - - if (conf->use_tlsv1 && !conf->use_sslv2 && !conf->use_sslv3) - method = TLSv1_client_method (); - else - method = SSLv23_client_method (); - - SSLContext = SSL_CTX_new (method); - - if (access (conf->cert_file, R_OK)) - { - if (errno != ENOENT) - { - perror ("access"); - return -1; - } - warn ("*** Warning: CertificateFile doesn't exist, can't verify server certificates\n"); - } - else - if (!SSL_CTX_load_verify_locations - (SSLContext, conf->cert_file, NULL)) - { - fprintf (stderr, "Error, SSL_CTX_load_verify_locations: %s\n", - ERR_error_string (ERR_get_error (), 0)); - return -1; - } - - if (!conf->use_sslv2) - options |= SSL_OP_NO_SSLv2; - if (!conf->use_sslv3) - options |= SSL_OP_NO_SSLv3; - if (!conf->use_tlsv1) - options |= SSL_OP_NO_TLSv1; - - SSL_CTX_set_options (SSLContext, options); - - /* we check the result of the verification after SSL_connect() */ - SSL_CTX_set_verify (SSLContext, SSL_VERIFY_NONE, 0); - return 0; -} -#endif /* HAVE_LIBSSL */ - -static int -socket_read (Socket_t * sock, char *buf, size_t len) -{ -#if HAVE_LIBSSL - if (sock->use_ssl) - return SSL_read (sock->ssl, buf, len); -#endif - return read (sock->fd, buf, len); -} - -static int -socket_write (Socket_t * sock, char *buf, size_t len) -{ -#if HAVE_LIBSSL - if (sock->use_ssl) - return SSL_write (sock->ssl, buf, len); -#endif - return write (sock->fd, buf, len); -} - -static void -socket_perror (const char *func, Socket_t *sock, int ret) -{ -#if HAVE_LIBSSL - int err; - - if (sock->use_ssl) - { - switch ((err = SSL_get_error (sock->ssl, ret))) - { - case SSL_ERROR_SYSCALL: - case SSL_ERROR_SSL: - if ((err = ERR_get_error ()) == 0) - { - if (ret == 0) - fprintf (stderr, "SSL_%s:got EOF\n", func); - else - fprintf (stderr, "SSL_%s:%d:%s\n", func, - errno, strerror (errno)); - } - else - fprintf (stderr, "SSL_%s:%d:%s\n", func, err, - ERR_error_string (err, 0)); - return; - default: - fprintf (stderr, "SSL_%s:%d:unhandled SSL error\n", func, err); - break; - } - return; - } -#else - (void) sock; -#endif - if (ret) - perror (func); - else - fprintf (stderr, "%s: unexpected EOF\n", func); -} - -/* simple line buffering */ -static int -buffer_gets (buffer_t * b, char **s) -{ - int n; - int start = b->offset; - - *s = b->buf + start; - - for (;;) - { - /* make sure we have enough data to read the \r\n sequence */ - if (b->offset + 1 >= b->bytes) - { - if (start != 0) - { - /* shift down used bytes */ - *s = b->buf; - - assert (start <= b->bytes); - n = b->bytes - start; - - if (n) - memmove (b->buf, b->buf + start, n); - b->offset -= start; - b->bytes = n; - start = 0; - } - - n = - socket_read (b->sock, b->buf + b->bytes, - sizeof (b->buf) - b->bytes); - - if (n <= 0) - { - socket_perror ("read", b->sock, n); - return -1; - } - - b->bytes += n; - } - - if (b->buf[b->offset] == '\r') - { - assert (b->offset + 1 < b->bytes); - if (b->buf[b->offset + 1] == '\n') - { - b->buf[b->offset] = 0; /* terminate the string */ - b->offset += 2; /* next line */ - if (Verbose) { - puts (*s); - fflush (stdout); - } - return 0; - } - } - - b->offset++; - } - /* not reached */ -} - -static int -parse_fetch (imap_t *imap, char *cmd) -{ - list_t *tmp, *list, *flags; - unsigned int uid = 0; - unsigned int mask = 0; - unsigned int size = 0; - message_t *cur; - - list = parse_list (cmd, 0); - - if (!is_list (list)) { - free_list (list); - return -1; - } - - for (tmp = list->child; tmp; tmp = tmp->next) - { - if (is_atom (tmp)) - { - if (!strcmp ("UID", tmp->val)) - { - tmp = tmp->next; - if (is_atom (tmp)) - { - uid = atoi (tmp->val); - if (uid < imap->minuid) - { - /* already saw this message */ - free_list (list); - return 0; - } - else if (uid > imap->maxuid) - imap->maxuid = uid; - } - else - fprintf (stderr, "IMAP error: unable to parse UID\n"); - } - else if (!strcmp ("FLAGS", tmp->val)) - { - tmp = tmp->next; - if (is_list (tmp)) - { - for (flags = tmp->child; flags; flags = flags->next) - { - if (is_atom (flags)) - { - if (!strcmp ("\\Seen", flags->val)) - mask |= D_SEEN; - else if (!strcmp ("\\Flagged", flags->val)) - mask |= D_FLAGGED; - else if (!strcmp ("\\Deleted", flags->val)) - mask |= D_DELETED; - else if (!strcmp ("\\Answered", flags->val)) - mask |= D_ANSWERED; - else if (!strcmp ("\\Draft", flags->val)) - mask |= D_DRAFT; - else if (!strcmp ("\\Recent", flags->val)) - mask |= D_RECENT; - else - fprintf (stderr, "IMAP error: unknown flag %s\n", - flags->val); - } - else - fprintf (stderr, "IMAP error: unable to parse FLAGS list\n"); - } - } - else - fprintf (stderr, "IMAP error: unable to parse FLAGS\n"); - } - else if (!strcmp ("RFC822.SIZE", tmp->val)) - { - tmp = tmp->next; - if (is_atom (tmp)) - size = atol (tmp->val); - } - } - } - - cur = calloc (1, sizeof (message_t)); - cur->next = imap->msgs; - imap->msgs = cur; - - if (mask & D_DELETED) - imap->deleted++; - - cur->uid = uid; - cur->flags = mask; - cur->size = size; - - free_list (list); - return 0; -} - -/* Keep this in sync with enum CAPABILITY */ -const char *cap_list[] = { - "LOGINDISABLED", - "UIDPLUS", - "NAMESPACE", -#if HAVE_LIBSSL - "AUTH=CRAM-MD5", - "STARTTLS", -#endif -}; - -static void -parse_capability (imap_t *imap, char *cmd) -{ - char *arg; - unsigned i; - - imap->caps = 0x80000000; - while ((arg = next_arg (&cmd))) - for (i = 0; i < as(cap_list); i++) - if (!strcmp (cap_list[i], arg)) - imap->caps |= 1 << i; -} - -static void -parse_response_code (imap_t * imap, char *s) -{ - char *arg, *p; - - if (*s != '[') - return; /* no response code */ - s++; - if (!(p = strchr (s, ']'))) - { - fprintf (stderr, "IMAP error: malformed response code\n"); - return; - } - *p++ = 0; - - arg = next_arg (&s); - - if (!strcmp ("UIDVALIDITY", arg)) - { - arg = next_arg (&s); - imap->uidvalidity = atol (arg); - } - else if (!strcmp ("CAPABILITY", arg)) - { - parse_capability (imap, s); - } - else if (!strcmp ("ALERT", arg)) - { - /* RFC2060 says that these messages MUST be displayed - * to the user - */ - for (; isspace ((unsigned char)*p); p++); - fprintf (stderr, "*** IMAP ALERT *** %s\n", p); - } -} - -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; - config_t *box; - int n; - - 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) { - printf (">>> %s", buf); - fflush (stdout); - } - n = socket_write (imap->sock, buf, strlen (buf)); - if (n <= 0) - { - socket_perror ("write", imap->sock, n); - return -1; - } - - for (;;) - { - next: - if (buffer_gets (imap->buf, &cmd)) - return -1; - - arg = next_arg (&cmd); - if (*arg == '*') - { - arg = next_arg (&cmd); - if (!arg) - { - fprintf (stderr, "IMAP error: unable to parse untagged response\n"); - return -1; - } - - if (!strcmp ("NAMESPACE", arg)) - { - imap->ns_personal = parse_list (cmd, &cmd); - imap->ns_other = parse_list (cmd, &cmd); - imap->ns_shared = parse_list (cmd, 0); - } - else if (!strcmp ("OK", arg) || !strcmp ("BAD", arg) || - !strcmp ("NO", arg) || !strcmp ("BYE", arg)) - { - parse_response_code (imap, cmd); - } - else if (!strcmp ("CAPABILITY", arg)) - { - parse_capability (imap, cmd); - } - else if (!strcmp ("LIST", arg)) - { - list_t *list, *lp; - int l; - - list = parse_list (cmd, &cmd); - if (list->val == LIST) - for (lp = list->child; lp; lp = lp->next) - if (is_atom (lp) && - !strcasecmp (lp->val, "\\NoSelect")) - { - free_list (list); - goto next; - } - free_list (list); - (void) next_arg (&cmd); /* skip delimiter */ - arg = next_arg (&cmd); - l = strlen (global.folder); - if (memcmp (arg, global.folder, l)) - goto next; - arg += l; - if (!memcmp (arg + strlen (arg) - 5, ".lock", 5)) - goto next; - for (box = boxes; box; box = box->next) - if (!strcmp (box->box, arg)) - goto next; - box = malloc (sizeof (config_t)); - memcpy (box, &global, sizeof (config_t)); - box->path = strdup (arg); - box->box = box->path; - box->next = boxes; - boxes = box; - } - else if ((arg1 = next_arg (&cmd))) - { - if (!strcmp ("EXISTS", arg1)) - imap->count = atoi (arg); - else if (!strcmp ("RECENT", arg1)) - imap->recent = atoi (arg); - else if (!strcmp ("FETCH", arg1)) - { - if (parse_fetch (imap, cmd)) - return -1; - } - } - else - { - fprintf (stderr, "IMAP error: unable to parse untagged response\n"); - return -1; - } - } -#if HAVE_LIBSSL - else if (*arg == '+') - { - char *resp; - - if (!imap->cram) - { - fprintf (stderr, "IMAP error, not doing CRAM-MD5 authentication\n"); - return -1; - } - resp = cram (cmd, imap->box->user, imap->box->pass); - - if (Verbose) { - printf (">+> %s\n", resp); - fflush (stdout); - } - n = socket_write (imap->sock, resp, strlen (resp)); - free (resp); - if (n <= 0) - { - socket_perror ("write", imap->sock, n); - return -1; - } - n = socket_write (imap->sock, "\r\n", 2); - if (n <= 0) - { - socket_perror ("write", imap->sock, n); - return -1; - } - imap->cram = 0; - } -#endif - else if (atoi (arg) != Tag) - { - fprintf (stderr, "IMAP error: wrong tag\n"); - return -1; - } - else - { - arg = next_arg (&cmd); - parse_response_code (imap, cmd); - if (!strcmp ("OK", arg)) - return 0; - return -1; - } - } - /* not reached */ -} - -#ifdef HAVE_LIBSSL -static int -start_tls (imap_t *imap, config_t * cfg) -{ - int ret; - - /* initialize SSL */ - if (init_ssl (cfg)) - return 1; - - imap->sock->ssl = SSL_new (SSLContext); - SSL_set_fd (imap->sock->ssl, imap->sock->fd); - if ((ret = SSL_connect (imap->sock->ssl)) <= 0) - { - socket_perror ("connect", imap->sock, ret); - return 1; - } - - /* verify the server certificate */ - if (verify_cert (imap->sock->ssl)) - return 1; - - imap->sock->use_ssl = 1; - puts ("SSL support enabled"); - return 0; -} -#endif - -imap_t * -imap_connect (config_t * cfg) -{ - int s; - struct sockaddr_in addr; - struct hostent *he; - imap_t *imap; - char *arg, *rsp; - int preauth; -#if HAVE_LIBSSL - int use_ssl; -#endif - int a[2]; - - imap = calloc (1, sizeof (imap_t)); - imap->box = cfg; - imap->sock = calloc (1, sizeof (Socket_t)); - imap->buf = calloc (1, sizeof (buffer_t)); - imap->buf->sock = imap->sock; - imap->sock->fd = -1; - - /* open connection to IMAP server */ - - if (cfg->tunnel) - { - info ("Starting tunnel '%s'...", cfg->tunnel); - fflush (stdout); - - if (socketpair (PF_UNIX, SOCK_STREAM, 0, a)) - { - perror ("socketpair"); - exit (1); - } - - if (fork () == 0) - { - if (dup2 (a[0], 0) == -1 || dup2 (a[0], 1) == -1) - { - _exit (127); - } - close (a[0]); - close (a[1]); - execl ("/bin/sh", "sh", "-c", cfg->tunnel, 0); - _exit (127); - } - - close (a[0]); - - imap->sock->fd = a[1]; - - info ("ok\n"); - } - else - { - memset (&addr, 0, sizeof (addr)); - addr.sin_port = htons (cfg->port); - addr.sin_family = AF_INET; - - info ("Resolving %s... ", cfg->host); - fflush (stdout); - he = gethostbyname (cfg->host); - if (!he) - { - perror ("gethostbyname"); - goto bail; - } - info ("ok\n"); - - addr.sin_addr.s_addr = *((int *) he->h_addr_list[0]); - - s = socket (PF_INET, SOCK_STREAM, 0); - - info ("Connecting to %s:%hu... ", inet_ntoa (addr.sin_addr), - ntohs (addr.sin_port)); - fflush (stdout); - if (connect (s, (struct sockaddr *) &addr, sizeof (addr))) - { - close (s); - perror ("connect"); - goto bail; - } - info ("ok\n"); - - imap->sock->fd = s; - } - -#if HAVE_LIBSSL - use_ssl = 0; - if (cfg->use_imaps) { - if (start_tls (imap, cfg)) - goto bail; - use_ssl = 1; - } -#endif - - /* read the greeting string */ - if (buffer_gets (imap->buf, &rsp)) - { - fprintf (stderr, "IMAP error: no greeting response\n"); - goto bail; - } - arg = next_arg (&rsp); - if (!arg || *arg != '*' || (arg = next_arg (&rsp)) == NULL) - { - fprintf (stderr, "IMAP error: invalid greeting response\n"); - goto bail; - } - preauth = 0; - if (!strcmp ("PREAUTH", arg)) - preauth = 1; - else if (strcmp ("OK", arg) != 0) - { - fprintf (stderr, "IMAP error: unknown greeting response\n"); - goto bail; - } - parse_response_code (imap, rsp); - if (!imap->caps && imap_exec (imap, "CAPABILITY")) - goto bail; - - if (!preauth) - { - -#if HAVE_LIBSSL - if (!cfg->use_imaps) - { - if (cfg->use_sslv2 || cfg->use_sslv3 || cfg->use_tlsv1) - { - /* always try to select SSL support if available */ - if (CAP(STARTTLS)) - { - if (imap_exec (imap, "STARTTLS")) - goto bail; - if (start_tls (imap, cfg)) - goto bail; - use_ssl = 1; - - if (imap_exec (imap, "CAPABILITY")) - goto bail; - } - else - { - if (cfg->require_ssl) - { - fprintf (stderr, "IMAP error: SSL support not available\n"); - goto bail; - } - else - warn ("IMAP warning: SSL support not available\n"); - } - } - } -#endif - - info ("Logging in...\n"); - - if (!cfg->pass) - { - /* - * if we don't have a global password set, prompt the user for - * it now. - */ - if (!global.pass) - { - char prompt[80]; - sprintf(prompt, "Password (mailbox %s@%s/%s): ", - cfg->user, cfg->host, cfg->box); - global.pass = getpass (prompt); - if (!global.pass) - { - perror ("getpass"); - exit (1); - } - if (!*global.pass) - { - fprintf (stderr, "Skipping %s, no password\n", cfg->path); - global.pass = NULL; /* force retry */ - goto bail; - } - /* - * getpass() returns a pointer to a static buffer. make a copy - * for long term storage. - */ - global.pass = strdup (global.pass); - } - cfg->pass = strdup (global.pass); - } - -#if HAVE_LIBSSL - if (CAP(CRAM)) - { - info ("Authenticating with CRAM-MD5\n"); - imap->cram = 1; - if (imap_exec (imap, "AUTHENTICATE CRAM-MD5")) - goto bail; - } - else if (imap->box->require_cram) - { - fprintf (stderr, "IMAP error: CRAM-MD5 authentication is not supported by server\n"); - goto bail; - } - else -#endif - { - if (CAP(NOLOGIN)) - { - fprintf (stderr, "Skipping %s, server forbids LOGIN\n", cfg->path); - goto bail; - } -#if HAVE_LIBSSL - if (!use_ssl) -#endif - warn ("*** IMAP Warning *** Password is being sent in the clear\n"); - if (imap_exec (imap, "LOGIN \"%s\" \"%s\"", cfg->user, cfg->pass)) - { - fprintf (stderr, "IMAP error: LOGIN failed\n"); - goto bail; - } - } - } /* !preauth */ - - /* get NAMESPACE info */ - if (!global.folder && cfg->use_namespace && CAP(NAMESPACE)) - { - if (imap_exec (imap, "NAMESPACE")) - goto bail; - } - return imap; - - bail: - imap_close (imap); - return 0; -} - -static int -mstrcmp (const char *s1, const char *s2) -{ - if (s1 == s2) - return 0; - if (!s1 || !s2) - return 1; - return strcmp (s1, s2); -} - -/* `box' is the config info for the maildrop to sync. `minuid' is the - * minimum UID to consider. in normal mode this will be 1, but in --fast - * mode we only fetch messages newer than the last one seen in the local - * mailbox. - */ -imap_t * -imap_open (config_t * box, unsigned int minuid, imap_t * imap, int imap_flags) -{ - if (imap) - { - /* determine whether or not we can reuse the existing session */ - if (mstrcmp (box->tunnel, imap->box->tunnel) || - mstrcmp (box->host, imap->box->host) || - mstrcmp (box->user, imap->box->user) || - box->port != imap->box->port -#if HAVE_LIBSSL - /* ensure that security requirements are met */ - || (box->require_ssl ^ imap->box->require_ssl) - || (box->require_cram ^ imap->box->require_cram) -#endif - ) - { - /* can't reuse */ - imap_close (imap); - } - else - { - /* reset mailbox-specific state info */ - imap->box = box; - imap->recent = 0; - imap->deleted = 0; - imap->count = 0; - imap->maxuid = 0; - free_message (imap->msgs); - imap->msgs = 0; - goto gotimap; - } - } - if (!(imap = imap_connect (box))) - return 0; - gotimap: - - if (global.folder) - imap->prefix = !strcmp (box->box, "INBOX") ? "" : global.folder; - else - { - imap->prefix = ""; - /* XXX for now assume personal namespace */ - if (imap->box->use_namespace && - is_list (imap->ns_personal) && - is_list (imap->ns_personal->child) && - is_atom (imap->ns_personal->child->child)) - imap->prefix = imap->ns_personal->child->child->val; - } - - info ("Selecting IMAP mailbox... "); - fflush (stdout); - if (imap_exec (imap, "SELECT \"%s%s\"", imap->prefix, box->box)) { - if (imap_flags & IMAP_CREATE) { - if (imap_exec (imap, "CREATE \"%s%s\"", imap->prefix, box->box)) - goto bail; - if (imap_exec (imap, "SELECT \"%s%s\"", imap->prefix, box->box)) - goto bail; - } else - goto bail; - } - info ("%d messages, %d recent\n", imap->count, imap->recent); - - info ("Reading IMAP mailbox index\n"); - imap->minuid = minuid; - if (imap->count > 0) - { - if (imap_exec (imap, "UID FETCH %d:* (FLAGS%s)", minuid, - (imap_flags & IMAP_GET_SIZE) ? " RFC822.SIZE" : "")) - goto bail; - } - - return imap; - - bail: - imap_close (imap); - return 0; -} - -void -imap_close (imap_t * imap) -{ - if (imap) - { - if (imap->sock->fd != -1) - { - imap_exec (imap, "LOGOUT"); - close (imap->sock->fd); - } - free (imap->sock); - free (imap->buf); - free_message (imap->msgs); - memset (imap, 0xff, sizeof (imap_t)); - free (imap); - } -} - -/* write a buffer stripping all \r bytes */ -static int -write_strip (int fd, char *buf, size_t len) -{ - size_t start = 0; - size_t end = 0; - ssize_t n; - - while (start < len) - { - while (end < len && buf[end] != '\r') - end++; - n = write (fd, buf + start, end - start); - if (n == -1) - { - perror ("write"); - return -1; - } - else if ((size_t) n != end - start) - { - /* short write, try again */ - start += n; - } - else - { - /* write complete */ - end++; - start = end; - } - } - return 0; -} - -static int -send_server (Socket_t * sock, const char *fmt, ...) -{ - char buf[128]; - char cmd[128]; - va_list ap; - int n; - - va_start (ap, fmt); - vsnprintf (buf, sizeof (buf), fmt, ap); - va_end (ap); - - snprintf (cmd, sizeof (cmd), "%d %s\r\n", ++Tag, buf); - if (Verbose) { - printf (">>> %s", cmd); - fflush (stdout); - } - n = socket_write (sock, cmd, strlen (cmd)); - if (n <= 0) - { - socket_perror ("write", sock, n); - return -1; - } - - return 0; -} - -int -imap_fetch_message (imap_t * imap, unsigned int uid, int fd) -{ - char *cmd; - char *arg; - size_t bytes; - size_t n; - char buf[1024]; - - send_server (imap->sock, "UID FETCH %d BODY.PEEK[]", uid); - - for (;;) - { - if (buffer_gets (imap->buf, &cmd)) - return -1; - if (*cmd == '*') - { - /* need to figure out how long the message is - * * <msgno> FETCH (RFC822 {<size>} - */ - - next_arg (&cmd); /* * */ - next_arg (&cmd); /* <msgno> */ - arg = next_arg (&cmd); /* FETCH */ - - if (strcasecmp ("FETCH", arg) != 0) - { - /* this is likely an untagged response, such as when new - * mail arrives in the middle of the session. just skip - * it for now. - * - * eg., - * "* 4000 EXISTS" - * "* 2 RECENT" - * - */ - info ("IMAP info: skipping untagged response: %s\n", arg); - continue; - } - - while ((arg = next_arg (&cmd)) && *arg != '{') - ; - if (!arg) - { - fprintf (stderr, "IMAP error: parse error getting size\n"); - return -1; - } - bytes = strtol (arg + 1, 0, 10); - - /* 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. - */ - if (write_strip (fd, imap->buf->buf + imap->buf->offset, n)) - { - /* write failed, message is not delivered */ - return -1; - } - - bytes -= n; - - /* mark that we used part of the buffer */ - imap->buf->offset += n; - - /* now read the rest of the message */ - while (bytes > 0) - { - n = bytes; - if (n > sizeof (buf)) - n = sizeof (buf); - n = socket_read (imap->sock, buf, n); - if (n > 0) - { - if (write_strip (fd, buf, n)) - { - /* write failed */ - return -1; - } - bytes -= n; - } - else - { - socket_perror ("read", imap->sock, n); - return -1; - } - } - - buffer_gets (imap->buf, &cmd); - } - else - { - arg = next_arg (&cmd); - if (!arg || atoi (arg) != Tag) - { - fprintf (stderr, "IMAP error: wrong tag\n"); - return -1; - } - arg = next_arg (&cmd); - if (!strcmp ("OK", arg)) - return 0; - return -1; - } - } - /* not reached */ -} - -/* add flags to existing flags */ -int -imap_set_flags (imap_t * imap, unsigned int uid, unsigned int flags) -{ - char buf[256]; - int i; - - buf[0] = 0; - for (i = 0; i < D_MAX; i++) - { - if (flags & (1 << i)) - snprintf (buf + strlen (buf), - sizeof (buf) - strlen (buf), "%s%s", - (buf[0] != 0) ? " " : "", Flags[i]); - } - - return imap_exec (imap, "UID STORE %d +FLAGS.SILENT (%s)", uid, buf); -} - -int -imap_expunge (imap_t * imap) -{ - return imap_exec (imap, "EXPUNGE"); -} - -int -imap_copy_message (imap_t * imap, unsigned int uid, const char *mailbox) -{ - return imap_exec (imap, "UID COPY %u \"%s%s\"", uid, imap->prefix, - mailbox); -} - -int -imap_append_message (imap_t * imap, int fd, message_t * msg) -{ - char *fmap; - int extra, uid, tuidl = 0; - char flagstr[128], tuid[128]; - char *s; - size_t i; - size_t start; - size_t len, sbreak = 0, ebreak = 0; - char *arg; - struct timeval tv; - pid_t pid = getpid(); - - len = msg->size; - /* ugh, we need to count the number of newlines */ - fmap = (char *)mmap (0, len, PROT_READ, MAP_PRIVATE, fd, 0); - if (!fmap) - { - perror ("mmap"); - return -1; - } - - extra = 0, i = 0; - if (!CAP(UIDPLUS)) - { - nloop: - start = i; - while (i < len) - if (fmap[i++] == '\n') - { - extra++; - if (i - 1 == start) - { - sbreak = ebreak = i - 1; - goto mktid; - } - if (!memcmp (fmap + start, "X-TUID: ", 8)) - { - extra -= (ebreak = i) - (sbreak = start) + 1; - goto mktid; - } - goto nloop; - } - /* invalid mesasge */ - goto bail; - mktid: - gettimeofday (&tv, 0); - tuidl = sprintf (tuid, "X-TUID: %08lx%05lx%04x\r\n", - tv.tv_sec, tv.tv_usec, pid); - extra += tuidl; - } - for (; i < len; i++) - if (fmap[i] == '\n') - extra++; - - flagstr[0] = 0; - if (msg->flags) - { - if (msg->flags & D_DELETED) - strcat (flagstr," \\Deleted"); - if (msg->flags & D_ANSWERED) - strcat (flagstr," \\Answered"); - if (msg->flags & D_SEEN) - strcat (flagstr," \\Seen"); - if (msg->flags & D_FLAGGED) - strcat (flagstr," \\Flagged"); - if (msg->flags & D_DRAFT) - strcat (flagstr," \\Draft"); - flagstr[0] = '('; - strcat (flagstr,") "); - } - - send_server (imap->sock, "APPEND %s%s %s{%d}", - imap->prefix, imap->box->box, flagstr, len + extra); - - if (buffer_gets (imap->buf, &s)) - goto bail; - - if (*s != '+') - { - fprintf (stderr, "IMAP error: expected `+' from server (aborting)\n"); - goto bail; - } - - i = 0; - if (!CAP(UIDPLUS)) - { - n1loop: - start = i; - while (i < sbreak) - if (fmap[i++] == '\n') - { - socket_write (imap->sock, fmap + start, i - 1 - start); - socket_write (imap->sock, "\r\n", 2); - goto n1loop; - } - socket_write (imap->sock, tuid, tuidl); - i = ebreak; - } - n2loop: - start = i; - while (i < len) - if (fmap[i++] == '\n') - { - socket_write (imap->sock, fmap + start, i - 1 - start); - socket_write (imap->sock, "\r\n", 2); - goto n2loop; - } - socket_write (imap->sock, fmap + start, len - start); - socket_write (imap->sock, "\r\n", 2); - - munmap (fmap, len); - - for (;;) - { - if (buffer_gets (imap->buf, &s)) - return -1; - - arg = next_arg (&s); - if (*arg == '*') - { - /* XXX just ignore it for now */ - } - else if (atoi (arg) != Tag) - { - fprintf (stderr, "IMAP error: wrong tag\n"); - return -1; - } - else - { - arg = next_arg (&s); - if (strcmp (arg, "OK")) - return -1; - arg = next_arg (&s); - if (*arg != '[') - break; - arg++; - if (strcasecmp ("APPENDUID", arg)) - { - fprintf (stderr, "IMAP error: expected APPENDUID\n"); - break; - } - arg = next_arg (&s); - if (!arg) - break; - if (atoi (arg) != (int) imap->uidvalidity) - { - fprintf (stderr, "IMAP error: UIDVALIDITY doesn't match APPENDUID\n"); - return -1; - } - arg = next_arg (&s); - if (!arg) - break; - uid = strtol (arg, &s, 10); - if (*s != ']') - { - /* parse error */ - break; - } - return uid; - } - } - - /* didn't receive an APPENDUID */ - send_server (imap->sock, - "UID SEARCH HEADER X-TUID %08lx%05lx%04x", - tv.tv_sec, tv.tv_usec, pid); - uid = 0; - for (;;) - { - if (buffer_gets (imap->buf, &s)) - return -1; - - arg = next_arg (&s); - if (*arg == '*') - { - arg = next_arg (&s); - if (!strcmp (arg, "SEARCH")) - { - arg = next_arg (&s); - if (!arg) - fprintf (stderr, "IMAP error: incomplete SEARCH response\n"); - else - uid = atoi (arg); - } - } - else if (atoi (arg) != (int) Tag) - { - fprintf (stderr, "IMAP error: wrong tag\n"); - return -1; - } - else - { - arg = next_arg (&s); - if (strcmp (arg, "OK")) - return -1; - return uid; - } - } - - return 0; - - bail: - munmap (fmap, len); - return -1; -} - -int -imap_list (imap_t * imap) -{ - return imap_exec (imap, "LIST \"\" \"%s*\"", global.folder); -} - diff --git a/src/isync.h b/src/isync.h @@ -1,6 +1,5 @@ -/* $Id$ - * - * isync - IMAP4 to maildir mailbox synchronizer +/* + * mbsync - mailbox synchronizer * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> * Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net> * @@ -18,219 +17,233 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * - * As a special exception, isync may be linked with the OpenSSL library, + * As a special exception, mbsync may be linked with the OpenSSL library, * despite that library's more restrictive license. */ +#define _GNU_SOURCE + #include <config.h> #include <sys/types.h> - -#include <db.h> - -#if HAVE_LIBSSL -# include <openssl/ssl.h> +#include <stdio.h> + +#define as(ar) (sizeof(ar)/sizeof(ar[0])) + +#if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ > 4) +# define ATTR_UNUSED __attribute__((unused)) +# define ATTR_NORETURN __attribute__((noreturn)) +# define ATTR_PRINTFLIKE(fmt,var) __attribute__((format(printf,fmt,var))) +#else +# define ATTR_UNUSED +# define ATTR_NORETURN +# define ATTR_PRINTFLIKE(fmt,var) #endif -typedef struct -{ - int fd; -#if HAVE_LIBSSL - SSL *ssl; - unsigned int use_ssl:1; -#endif -} Socket_t; - -typedef struct -{ - Socket_t *sock; - int bytes; - int offset; - char buf[1024]; -} -buffer_t; - -typedef struct config config_t; -typedef struct mailbox mailbox_t; -typedef struct message message_t; - -struct config -{ - char *maildir; - char *path; /* path relative to .maildir, or absolute path */ - char *host; - int port; - char *user; - char *pass; - char *folder; - char *box; - char *inbox; - char *alias; - char *copy_deleted_to; - char *tunnel; - unsigned int max_messages; - off_t max_size; - config_t *next; -#if HAVE_LIBSSL - char *cert_file; - unsigned int use_imaps:1; - unsigned int require_ssl:1; - unsigned int use_sslv2:1; - unsigned int use_sslv3:1; - unsigned int use_tlsv1:1; - unsigned int require_cram:1; -#endif - unsigned int use_namespace:1; - unsigned int expunge:1; - unsigned int delete:1; - unsigned int wanted:1; +#define EXE "mbsync" + +typedef struct { + const char *file; + FILE *fp; + char *buf; + int bufl; + int line; + char *cmd, *val, *rest; +} conffile_t; + +#define OP_NEW (1<<0) +#define OP_RENEW (1<<1) +#define OP_DELETE (1<<2) +#define OP_FLAGS (1<<3) +#define OP_MASK_TYPE (OP_NEW|OP_RENEW|OP_DELETE|OP_FLAGS) /* asserted in the target ops */ +#define OP_EXPUNGE (1<<4) +#define OP_CREATE (1<<5) +#define XOP_PUSH (1<<6) +#define XOP_PULL (1<<7) +#define XOP_MASK_DIR (XOP_PUSH|XOP_PULL) +#define XOP_HAVE_TYPE (1<<8) +#define XOP_HAVE_EXPUNGE (1<<9) +#define XOP_HAVE_CREATE (1<<10) + +typedef struct driver driver_t; + +typedef struct store_conf { + struct store_conf *next; + char *name; + driver_t *driver; + const char *path; /* should this be here? its interpretation is driver-specific */ + char *map_inbox; + char *trash; + unsigned max_size; /* off_t is overkill */ + unsigned trash_remote_new:1, trash_only_new:1; +} store_conf_t; + +typedef struct string_list { + struct string_list *next; + char string[1]; +} string_list_t; + +typedef struct channel_conf { + struct channel_conf *next; + char *name; + store_conf_t *master, *slave; + char *master_name, *slave_name; + char *sync_state; + string_list_t *patterns; + int mops, sops; + unsigned max_messages; /* for slave only */ +} channel_conf_t; + +typedef struct group_conf { + struct group_conf *next; + char *name; + string_list_t *channels; +} group_conf_t; + +/* For message->flags */ +/* Keep the mailbox driver flag definitions in sync! */ +/* The order is according to alphabetical maildir flag sort */ +#define F_DRAFT (1<<0) /* Draft */ +#define F_FLAGGED (1<<1) /* Flagged */ +#define F_ANSWERED (1<<2) /* Replied */ +#define F_SEEN (1<<3) /* Seen */ +#define F_DELETED (1<<4) /* Trashed */ +#define NUM_FLAGS 5 + +/* For message->status */ +#define M_RECENT (1<<0) /* unsyncable flag; maildir_* depend on this being 1<<0 */ +#define M_DEAD (1<<1) /* expunged */ +#define M_FLAGS (1<<2) /* flags fetched */ +#define M_PROCESSED (1<<3) /* registered in pair */ +#define M_NOT_SYNCED (1<<4) /* not in remote mailbox, yet */ +#define M_EXPIRED (1<<5) /* kicked out by MaxMessages */ + +typedef struct message { + struct message *next; + /* string_list_t *keywords; */ + size_t size; /* zero implies "not fetched" */ + int uid; + unsigned char flags, status; +} message_t; + +/* For opts, both in store and driver_t->select() */ +#define OPEN_OLD (1<<0) +#define OPEN_NEW (1<<1) +#define OPEN_FLAGS (1<<2) +#define OPEN_SIZE (1<<3) +#define OPEN_CREATE (1<<4) +#define OPEN_EXPUNGE (1<<5) +#define OPEN_SETFLAGS (1<<6) +#define OPEN_APPEND (1<<7) + +typedef struct store { + store_conf_t *conf; /* foreign */ + + /* currently open mailbox */ + const char *name; /* foreign! maybe preset? */ + char *path; /* own */ + message_t *msgs; /* own */ + int uidvalidity; + unsigned char opts; /* maybe preset? */ + /* note that the following do _not_ reflect stats from msgs, but mailbox totals */ + int count; /* # of messages */ + int recent; /* # of recent messages - don't trust this beyond the initial read */ +} store_t; + +typedef struct { + char *data; + int len; + unsigned char flags; + unsigned char crlf:1; +} msg_data_t; + +#define DRV_OK 0 +#define DRV_MSG_BAD -1 +#define DRV_BOX_BAD -2 +#define DRV_STORE_BAD -3 + +struct driver { + int (*parse_store)( conffile_t *cfg, store_conf_t **storep, int *err ); + store_t *(*open_store)( store_conf_t *conf, store_t *oldctx ); + void (*close_store)( store_t *ctx ); + int (*list)( store_t *ctx, string_list_t **boxes ); + void (*prepare)( store_t *ctx, int opts ); + int (*select)( store_t *ctx, int minuid, int maxuid, int *excs, int nexcs ); + int (*fetch_msg)( store_t *ctx, message_t *msg, msg_data_t *data ); + int (*store_msg)( store_t *ctx, msg_data_t *data, int *uid ); /* if uid is null, store to trash */ + int (*set_flags)( store_t *ctx, message_t *msg, int uid, int add, int del ); /* msg can be null, therefore uid as a fallback */ + int (*trash_msg)( store_t *ctx, message_t *msg ); /* This may expunge the original message immediately, but it needn't to */ + int (*check)( store_t *ctx ); /* IMAP-style: flush */ + int (*close)( store_t *ctx ); /* IMAP-style: expunge inclusive */ }; -/* struct representing local mailbox file */ -struct mailbox -{ - DB *db; - char *path; - message_t *msgs; - int lockfd; - unsigned int deleted; /* # of deleted messages */ - unsigned int uidvalidity; - unsigned int maxuid; /* largest uid we know about */ - unsigned int uidseen : 1; /* flag indicating whether or not we saw a - valid value for UIDVALIDITY */ -}; -/* 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; - size_t size; - message_t *next; - unsigned int processed:1; /* message has already been evaluated */ - unsigned int new:1; /* message is in the new/ subdir */ - unsigned int dead:1; /* message doesn't exist on the server */ - unsigned int wanted:1; /* when using MaxMessages, keep this message */ -}; +/* main.c */ -/* struct used for parsing IMAP lists */ -typedef struct _list list_t; +extern int Pid; +extern char Hostname[256]; +extern const char *Home; -#define NIL (void*)0x1 -#define LIST (void*)0x2 -struct _list -{ - char *val; - list_t *next; - list_t *child; -}; +/* util.c */ -/* imap connection info */ -typedef struct -{ - Socket_t *sock; - 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 */ - char *prefix; /* namespace prefix */ - unsigned int deleted; /* # of deleted messages */ - unsigned int uidvalidity; - unsigned int maxuid; - unsigned int minuid; - /* NAMESPACE info */ - list_t *ns_personal; - list_t *ns_other; - list_t *ns_shared; - unsigned int caps; -#if HAVE_LIBSSL - unsigned int cram:1; -#endif -} -imap_t; - -/* Keep in sync with cap_list */ -enum CAPABILITY { - NOLOGIN, - UIDPLUS, - NAMESPACE, -#if HAVE_LIBSSL - CRAM, - STARTTLS, -#endif -}; +extern int Verbose, Quiet, Debug; -/* flags for sync_mailbox */ -#define SYNC_DELETE (1<<0) /* delete local that don't exist on server */ -#define SYNC_EXPUNGE (1<<1) /* don't fetch deleted messages */ +void debug( const char *, ... ); +void info( const char *, ... ); +void infoc( char ); +void warn( const char *, ... ); -/* flags for maildir_open */ -#define OPEN_FAST (1<<0) /* fast open - don't parse */ -#define OPEN_CREATE (1<<1) /* create mailbox if nonexistent */ +char *next_arg( char ** ); -/* flags for imap_open */ -#define IMAP_CREATE (1<<0) /* Create remote mailboxes if necessary */ -#define IMAP_GET_SIZE (1<<1) /* Request the RFC 822 SIZE */ +void add_string_list( string_list_t **list, const char *str ); +void free_string_list( string_list_t *list ); +void free_generic_messages( message_t * ); -extern config_t global; -extern config_t *boxes; -extern char Hostname[256]; -extern int Verbose, Quiet; +void strip_cr( msg_data_t *msgdata ); -extern void info (const char *, ...); -extern void infoc (char); -extern void warn (const char *, ...); +void *nfmalloc( size_t sz ); +void *nfcalloc( size_t sz ); +void *nfrealloc( void *mem, size_t sz ); +char *nfstrdup( const char *str ); +int nfvasprintf( char **str, const char *fmt, va_list va ); +int nfasprintf( char **str, const char *fmt, ... ); +int nfsnprintf( char *buf, int blen, const char *fmt, ... ); +void ATTR_NORETURN oob( void ); -#if HAVE_LIBSSL -extern SSL_CTX *SSLContext; +char *expand_strdup( const char *s ); -char *cram (const char *, const char *, const char *); -#endif +void sort_ints( int *arr, int len ); + +void arc4_init( void ); +unsigned char arc4_getbyte( void ); + +/* sync.c */ + +#define SYNC_OK 0 +#define SYNC_FAIL 1 +#define SYNC_MASTER_BAD 2 +#define SYNC_SLAVE_BAD 3 + +int sync_boxes( store_t *, const char *, + store_t *, const char *, + channel_conf_t * ); + +/* config.c */ + +extern channel_conf_t *channels; +extern group_conf_t *groups; +extern int global_mops, global_sops; +extern char *global_sync_state; + +int parse_bool( conffile_t *cfile ); +int parse_int( conffile_t *cfile ); +int parse_size( conffile_t *cfile ); +int getcline( conffile_t *cfile ); +int merge_ops( int cops, int *mops, int *sops ); +int load_config( const char *filename, int pseudo ); +void parse_generic_store( store_conf_t *store, conffile_t *cfg, int *err ); -char *next_arg (char **); - -int sync_mailbox (mailbox_t *, imap_t *, int, unsigned int, unsigned int); - -void load_config (const char *, int *); -char * expand_strdup (const char *s); -config_t *find_box (const char *); - -void imap_close (imap_t *); -int imap_copy_message (imap_t * imap, unsigned int uid, const char *mailbox); -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_connect (config_t *); -imap_t *imap_open (config_t *, unsigned int, imap_t *, int); -int imap_append_message (imap_t *, int, message_t *); -int imap_list (imap_t *); - -mailbox_t *maildir_open (const char *, int flags); -int maildir_expunge (mailbox_t *, int); -int maildir_set_uidvalidity (mailbox_t *, unsigned int uidvalidity); -void maildir_close (mailbox_t *); -int maildir_update_maxuid (mailbox_t * mbox); - -message_t * find_msg (message_t * list, unsigned int uid); -void free_message (message_t *); - -/* parse an IMAP list construct */ -list_t * parse_list (char *s, char **end); -int is_atom (list_t *list); -int is_list (list_t *list); -int is_nil (list_t *list); -void free_list (list_t *list); - -#define strfcpy(a,b,c) {strncpy(a,b,c);(a)[c-1]=0;} +/* drv_*.c */ +extern driver_t maildir_driver, imap_driver; diff --git a/src/list.c b/src/list.c @@ -1,179 +0,0 @@ -/* $Id$ - * - * isync - IMAP4 to maildir mailbox synchronizer - * Copyright (C) 2000-2002 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 - * - * As a special exception, isync may be linked with the OpenSSL library, - * despite that library's more restrictive license. - */ - -#include "isync.h" - -#include <ctype.h> -#include <stdlib.h> -#include <string.h> - -static char * -skip_string (char *s) -{ - while (*s && *s != '"') - s++; - return s; -} - -list_t * -parse_list (char *s, char **end) -{ - int level = 1; - list_t *cur; - list_t **list; - char *b; - - cur = calloc (1, sizeof (list_t)); - while (isspace ((unsigned char) *s)) - s++; - if (*s == '(') - { - /* start of list. find the end of the list */ - s++; - b = s; /* save beginning */ - cur->val = LIST; - while (*s) - { - if (*s == '(') - { - level++; - } - else if (*s == ')') - { - level--; - if (level == 0) - break; - } - else if (*s == '"') - { - s = skip_string (s + 1); - if (!*s) - { - /* parse error */ - free (cur); - return NULL; - } - } - s++; - } - if (level != 0) - { - free (cur); /* parse error */ - return NULL; - } - *s++ = 0; - - list = &cur->child; - while (*b) - { - *list = parse_list (b, &b); - if (*list == NULL) - { - /* parse error */ - free (cur); - return NULL; - } - while (*list) - list = &(*list)->next; - } - } - else if (*s == '"') - { - /* quoted string */ - s++; - cur->val = s; - s = skip_string (s); - if (!*s) - { - /* parse error */ - free (cur); - return NULL; - } - *s++ = 0; - cur->val = strdup (cur->val); - } - else - { - /* atom */ - cur->val = s; - while (*s && !isspace ((unsigned char) *s)) - s++; - if (*s) - *s++ = 0; - if (strcmp ("NIL", cur->val)) - cur->val = strdup (cur->val); - else - cur->val = NIL; - } - if (end) - *end = s; - return cur; -} - -int -is_atom (list_t * list) -{ - return (list && list->val && list->val != NIL && list->val != LIST); -} - -int -is_list (list_t * list) -{ - return (list && list->val == LIST); -} - -int -is_nil (list_t * list) -{ - return (list && list->val == NIL); -} - -void -free_list (list_t * list) -{ - list_t *tmp; - - while (list) - { - tmp = list; - list = list->next; - if (is_list (tmp)) - free_list (tmp->child); - else if (is_atom (tmp)) - free (tmp->val); - free (tmp); - } -} - -#if TEST -int -main (int argc, char **argv) -{ - char buf[256]; - list_t *list; - - strcpy (buf, - "((compound list) atom NIL \"string with a (\" (another list))"); - list = parse_list (buf, 0); -} -#endif diff --git a/src/maildir.c b/src/maildir.c @@ -1,459 +0,0 @@ -/* $Id$ - * - * isync - IMAP4 to maildir mailbox synchronizer - * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> - * Copyright (C) 2002-2003 Oswald Buddenhagen <ossi@users.sf.net> - * - * 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 - * - * As a special exception, isync may be linked with the OpenSSL library, - * despite that library's more restrictive license. - */ - -#include "isync.h" -#include "dotlock.h" - -#include <limits.h> -#include <stdlib.h> -#include <string.h> -#include <dirent.h> -#include <fcntl.h> -#include <stdio.h> -#include <unistd.h> -#include <sys/stat.h> -#include <errno.h> -#include <time.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++; - } - } -} - -/* - * There are three possible results of this function: - * >1 uid was already seen - * 0 uid was not yet seen - * -1 unable to read uid because of some other error - */ - -static int -read_uid (const char *path, const char *file, unsigned int *uid /* out */) -{ - char full[_POSIX_PATH_MAX]; - int fd; - int ret = -1; - int len; - char buf[64], *ptr; - - snprintf (full, sizeof (full), "%s/%s", path, file); - fd = open (full, O_RDONLY); - if (fd == -1) - { - if (errno != ENOENT) - { - perror (full); - return -1; - } - return 0; /* doesn't exist */ - } - len = read (fd, buf, sizeof (buf) - 1); - if (len == -1) - perror ("read"); - else - { - buf[len] = 0; - errno = 0; - *uid = strtoul (buf, &ptr, 10); - if (errno) - perror ("strtoul"); - else if (ptr && *ptr == '\n') - ret = 1; - /* else invalid value */ - } - close (fd); - return ret; -} - -/* - * open a maildir mailbox. - * if OPEN_FAST is set, 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. - * if OPEN_CREATE is set, we create the mailbox if it doesn't already exist. - */ -mailbox_t * -maildir_open (const char *path, int flags) -{ - char buf[_POSIX_PATH_MAX]; - DIR *d; - struct dirent *e; - message_t **cur; - message_t *p; - mailbox_t *m; - char *s; - int count = 0; - struct stat sb; - const char *subdirs[] = { "cur", "new", "tmp" }; - int i, ret; - DBT key, value; - - m = calloc (1, sizeof (mailbox_t)); - m->lockfd = -1; - /* filename expansion happens here, not in the config parser */ - m->path = expand_strdup (path); - - if (stat (m->path, &sb)) - { - if (errno == ENOENT && (flags & OPEN_CREATE)) - { - if (mkdir (m->path, S_IRUSR | S_IWUSR | S_IXUSR)) - { - fprintf (stderr, "ERROR: mkdir %s: %s (errno %d)\n", - m->path, strerror (errno), errno); - goto err; - } - - for (i = 0; i < 3; i++) - { - snprintf (buf, sizeof (buf), "%s/%s", m->path, subdirs[i]); - if (mkdir (buf, S_IRUSR | S_IWUSR | S_IXUSR)) - { - fprintf (stderr, "ERROR: mkdir %s: %s (errno %d)\n", - buf, strerror (errno), errno); - goto err; - } - } - - } - else - { - fprintf (stderr, "ERROR: stat %s: %s (errno %d)\n", m->path, - strerror (errno), errno); - goto err; - } - } - else - { - /* check to make sure this looks like a valid maildir box */ - for (i = 0; i < 3; i++) - { - snprintf (buf, sizeof (buf), "%s/%s", m->path, subdirs[i]); - if (stat (buf, &sb)) - { - fprintf (stderr, "ERROR: stat %s: %s (errno %d)\n", buf, - strerror (errno), errno); - fprintf (stderr, - "ERROR: %s does not appear to be a valid maildir style mailbox\n", - m->path); - goto err; - } - } - } - - /* - * we need a mutex on the maildir because of the state files that isync - * uses. - */ - snprintf (buf, sizeof (buf), "%s/isynclock", m->path); - if (dotlock_lock (buf, &m->lockfd)) - goto err; - - /* check for the uidvalidity value */ - i = read_uid (m->path, "isyncuidvalidity", &m->uidvalidity); - if (i == -1) - goto err; - else if (i > 0) - m->uidseen = 1; - - /* load the current maxuid */ - if (read_uid (m->path, "isyncmaxuid", &m->maxuid) == -1) - goto err; - - snprintf (buf, sizeof (buf), "%s/isyncuidmap.db", m->path); - if (db_create (&m->db, 0, 0)) { - fputs ("dbcreate failed\n", stderr); - goto err; - } - if ((ret = m->db->set_pagesize (m->db, 4096)) != 0 || - (ret = m->db->set_h_ffactor (m->db, 40)) != 0 || - (ret = m->db->set_h_nelem (m->db, 1)) != 0) { - fputs ("Error configuring database\n", stderr); - goto err; - } - m->db->open (m->db, buf, 0, DB_HASH, DB_CREATE, S_IRUSR | S_IWUSR); - if (m->db == NULL) - { - fputs ("ERROR: unable to open UID db\n", stderr); - goto err; - } - - if (flags & OPEN_FAST) - return m; - - cur = &m->msgs; - for (; count < 2; count++) - { - /* read the msgs from the new subdir */ - snprintf (buf, sizeof (buf), "%s/%s", m->path, - (count == 0) ? "new" : "cur"); - d = opendir (buf); - if (!d) - { - perror ("opendir"); - goto err; - } - 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 = 0; - p->new = (count == 0); - - /* determine the UID for this message. The basename (sans - * flags) is used as the key in the db - */ - memset (&key, 0, sizeof(key)); - memset (&value, 0, sizeof(value)); - key.data = p->file; - s = strchr (p->file, ':'); - key.size = s ? (size_t) (s - p->file) : strlen (p->file); - ret = m->db->get (m->db, 0, &key, &value, 0); - if (ret == DB_NOTFOUND) { - /* Every locally generated message triggers this ... */ - /*warn ("Warning: no UID for message %.*s\n", - key.size, p->file);*/ - } else if (ret) { - fprintf (stderr, "Unexpected error (%d) from db_get(%.*s)\n", - ret, key.size, p->file); - } else if (ret == 0) { - p->uid = *((int *) value.data); - if (p->uid > m->maxuid) - m->maxuid = p->uid; - } - if (s) - parse_info (p, s + 1); - if (p->flags & D_DELETED) - m->deleted++; - cur = &p->next; - } - closedir (d); - } - return m; - - err: - if (m->db) - m->db->close (m->db, 0); - dotlock_unlock (&m->lockfd); - free (m->path); - free (m); - return NULL; -} - -/* 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 *s; - DBT key; - char path[_POSIX_PATH_MAX]; - - while (*cur) - { - if ((dead == 0 && (*cur)->flags & D_DELETED) || - (dead && (*cur)->dead)) - { - tmp = *cur; - snprintf (path, sizeof (path), "%s/%s/%s", - mbox->path, tmp->new ? "new" : "cur", tmp->file); - if (unlink (path)) - perror (path); - /* remove the message from the UID map */ - memset (&key, 0, sizeof(key)); - key.data = tmp->file; - s = strchr (tmp->file, ':'); - key.size = s ? (size_t) (s - tmp->file) : strlen (key.data); - mbox->db->del (mbox->db, 0, &key, 0); - mbox->db->sync (mbox->db, 0); - *cur = (*cur)->next; - free (tmp->file); - free (tmp); - } - else - cur = &(*cur)->next; - } - return 0; -} - -int -maildir_update_maxuid (mailbox_t * mbox) -{ - int fd; - char buf[64]; - size_t len; - char path[_POSIX_PATH_MAX]; - int ret = 0; - - snprintf (path, sizeof (path), "%s/isyncmaxuid", mbox->path); - fd = open (path, O_WRONLY | O_CREAT, 0600); - if (fd == -1) - { - perror ("open"); - return -1; - } - - /* write out the file */ - snprintf (buf, sizeof (buf), "%u\n", mbox->maxuid); - len = write (fd, buf, strlen (buf)); - if (len == (size_t) - 1) - { - perror ("write"); - ret = -1; - } - - if (close (fd)) - ret = -1; - - return ret; -} - -#define _24_HOURS (3600 * 24) - -static void -maildir_clean_tmp (const char *mbox) -{ - char path[_POSIX_PATH_MAX]; - DIR *dirp; - struct dirent *entry; - struct stat st; - time_t now; - - snprintf (path, sizeof (path), "%s/tmp", mbox); - dirp = opendir (path); - if (dirp == NULL) - { - fprintf (stderr, "maildir_clean_tmp: opendir: %s: %s (errno %d)\n", - path, strerror (errno), errno); - return; - } - /* assuming this scan will take less than a second, we only need to - * check the time once before the following loop. - */ - time (&now); - while ((entry = readdir (dirp))) - { - snprintf (path, sizeof (path), "%s/tmp/%s", mbox, entry->d_name); - if (stat (path, &st)) - fprintf (stderr, "maildir_clean_tmp: stat: %s: %s (errno %d)\n", - path, strerror (errno), errno); - else if (S_ISREG (st.st_mode) && now - st.st_ctime >= _24_HOURS) - { - /* this should happen infrequently enough that it won't be - * bothersome to the user to display when it occurs. - */ - info ("Notice: removing stale file %s\n", path); - if (unlink (path)) - fprintf (stderr, - "maildir_clean_tmp: unlink: %s: %s (errno %d)\n", - path, strerror (errno), errno); - } - } - closedir(dirp); -} - -void -maildir_close (mailbox_t * mbox) -{ - if (mbox->db) - mbox->db->close (mbox->db, 0); - - /* release the mutex on the mailbox */ - dotlock_unlock (&mbox->lockfd); - - /* per the maildir(5) specification, delivery agents are supposed to - * set a 24-hour timer on items placed in the `tmp' directory. - */ - maildir_clean_tmp (mbox->path); - - free (mbox->path); - free_message (mbox->msgs); - memset (mbox, 0xff, sizeof (mailbox_t)); - free (mbox); -} - -int -maildir_set_uidvalidity (mailbox_t * mbox, unsigned int uidvalidity) -{ - char path[_POSIX_PATH_MAX]; - char buf[16]; - int fd; - int ret; - - snprintf (path, sizeof (path), "%s/isyncuidvalidity", mbox->path); - fd = open (path, O_WRONLY | O_CREAT | O_EXCL, 0600); - if (fd == -1) - { - perror ("open"); - return -1; - } - snprintf (buf, sizeof (buf), "%u\n", uidvalidity); - - ret = write (fd, buf, strlen (buf)); - - if (ret == -1) - perror ("write"); - else if ((size_t) ret != strlen (buf)) - ret = -1; - else - ret = 0; - - if (close (fd)) - { - perror ("close"); - ret = -1; - } - - if (ret) - if (unlink (path)) - perror ("unlink"); - - return (ret); -} diff --git a/src/main.c b/src/main.c @@ -1,6 +1,5 @@ -/* $Id$ - * - * isync - IMAP4 to maildir mailbox synchronizer +/* + * mbsync - mailbox synchronizer * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> * Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net> * @@ -18,473 +17,597 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * - * As a special exception, isync may be linked with the OpenSSL library, + * As a special exception, mbsync may be linked with the OpenSSL library, * despite that library's more restrictive license. */ #include "isync.h" -#include <sys/types.h> -#include <sys/stat.h> #include <stdlib.h> #include <unistd.h> -#include <stdarg.h> -#include <limits.h> -#include <pwd.h> -#include <stdio.h> -#include <errno.h> #include <string.h> -#include <ctype.h> -#include <dirent.h> - -int Quiet; - -void -info (const char *msg, ...) -{ - va_list va; - - if (!Quiet) - { - va_start (va, msg); - vprintf (msg, va); - va_end (va); - } -} - -void -infoc (char c) -{ - if (!Quiet) - putchar (c); -} -void -warn (const char *msg, ...) -{ - va_list va; - - if (Quiet < 2) - { - va_start (va, msg); - vfprintf (stderr, msg, va); - va_end (va); - } -} - -#if HAVE_GETOPT_LONG -# define _GNU_SOURCE -# include <getopt.h> -struct option Opts[] = { - {"all", 0, NULL, 'a'}, - {"list", 0, NULL, 'l'}, - {"config", 1, NULL, 'c'}, - {"create", 0, NULL, 'C'}, - {"create-local", 0, NULL, 'L'}, - {"create-remote", 0, NULL, 'R'}, - {"delete", 0, NULL, 'd'}, - {"expunge", 0, NULL, 'e'}, - {"fast", 0, NULL, 'f'}, - {"help", 0, NULL, 'h'}, - {"remote", 1, NULL, 'r'}, - {"folder", 1, NULL, 'F'}, - {"maildir", 1, NULL, 'M'}, - {"one-to-one", 0, NULL, '1'}, - {"inbox", 1, NULL, 'I'}, - {"host", 1, NULL, 's'}, - {"port", 1, NULL, 'p'}, - {"quiet", 0, NULL, 'q'}, - {"user", 1, NULL, 'u'}, - {"version", 0, NULL, 'v'}, - {"verbose", 0, NULL, 'V'}, - {0, 0, 0, 0} -}; -#endif - -config_t global; -char Hostname[256]; -int Verbose = 0; +int Pid; /* for maildir and imap */ +char Hostname[256]; /* for maildir */ +const char *Home; /* for config */ static void -version (void) +version( void ) { - puts (PACKAGE " " VERSION); - exit (0); + puts( PACKAGE " " VERSION ); + exit( 0 ); } static void -usage (int code) +usage( int code ) { - fputs ( -PACKAGE " " VERSION " IMAP4 to maildir synchronizer\n" + fputs( +PACKAGE " " VERSION " - mailbox synchronizer\n" "Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org>\n" "Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net>\n" +"Copyright (C) 2004 Theodore Ts'o <tytso@mit.edu>\n" "usage:\n" -" " PACKAGE " [ flags ] mailbox [mailbox ...]\n" -" " PACKAGE " [ flags ] -a\n" -" " PACKAGE " [ flags ] -l\n" -" -a, --all synchronize all defined mailboxes\n" -" -l, --list list all defined mailboxes and exit\n" -" -L, --create-local create local maildir mailbox if nonexistent\n" -" -R, --create-remote create remote imap mailbox if nonexistent\n" -" -C, --create create both local and remote mailboxes if nonexistent\n" -" -d, --delete delete local msgs that don't exist on the server\n" -" -e, --expunge expunge deleted messages\n" -" -f, --fast only fetch new messages\n" -" -r, --remote BOX remote mailbox\n" -" -F, --folder DIR remote IMAP folder containing mailboxes\n" -" -M, --maildir DIR local directory containing mailboxes\n" -" -1, --one-to-one map every IMAP <folder>/box to <maildir>/box\n" -" -I, --inbox BOX map IMAP INBOX to <maildir>/BOX (exception to -1)\n" -" -s, --host HOST IMAP server address\n" -" -p, --port PORT server IMAP port\n" -" -u, --user USER IMAP user name\n" -" -c, --config CONFIG read an alternate config file (default: ~/.isyncrc)\n" +" " EXE " [flags] {{channel[:box,...]|group} ...|-a}\n" +" -a, --all operate on all defined channels\n" +" -l, --list list mailboxes instead of syncing them\n" +" -n, --new propagate new messages\n" +" -d, --delete propagate message deletions\n" +" -f, --flags propagate message flag changes\n" +" -N, --renew propagate previously not propagated new messages\n" +" -L, --pull propagate from master to slave\n" +" -H, --push propagate from slave to master\n" +" -C, --create create mailboxes if nonexistent\n" +" -X, --expunge expunge deleted messages\n" +" -c, --config CONFIG read an alternate config file (default: ~/." EXE "rc)\n" +" -D, --debug print debugging messages\n" " -V, --verbose verbose mode (display network traffic)\n" " -q, --quiet don't display progress info\n" " -v, --version display version\n" " -h, --help display this help message\n" -"Compile time options:\n" +"\nIf neither --pull nor --push are specified, both are active.\n" +"If neither --new, --delete, --flags nor --renew are specified, all are active.\n" +"Direction and operation can be concatenated like --pull-new, etc.\n" +"--create and --expunge can be suffixed with -master/-slave. Read the man page.\n" +"\nSupported mailbox formats are: IMAP4rev1, Maildir\n" +"\nCompile time options:\n" #if HAVE_LIBSSL " +HAVE_LIBSSL\n" #else " -HAVE_LIBSSL\n" #endif - , code ? stderr : stdout); - exit (code); + , code ? stderr : stdout ); + exit( code ); } -char * -next_arg (char **s) +static int +matches( const char *t, const char *p ) { - char *ret; - - if (!s) - return 0; - if (!*s) - return 0; - while (isspace ((unsigned char) **s)) - (*s)++; - if (!**s) - { - *s = 0; - return 0; - } - if (**s == '"') - { - ++*s; - ret = *s; - *s = strchr (*s, '"'); - } - else - { - ret = *s; - while (**s && !isspace ((unsigned char) **s)) - (*s)++; - } - if (*s) - { - if (**s) - *(*s)++ = 0; - if (!**s) - *s = 0; - } - return ret; + for (;;) { + if (!*p) + return !*t; + if (*p == '*') { + p++; + do { + if (matches( t, p )) + return 1; + } while (*t++); + return 0; + } else if (*p == '%') { + p++; + do { + if (*t == '.' || *t == '/') /* this is "somewhat" hacky ... */ + return 0; + if (matches( t, p )) + return 1; + } while (*t++); + return 0; + } else { + if (*p != *t) + return 0; + p++, t++; + } + } } -int -main (int argc, char **argv) +static string_list_t * +filter_boxes( string_list_t *boxes, string_list_t *patterns ) { - int i; - int ret; - config_t *box = 0; - mailbox_t *mail = 0; - imap_t *imap = 0; - int expunge = 0; /* by default, don't delete anything */ - int fast = 0; - int delete = 0; - char *config = 0; - struct passwd *pw; - int all = 0; - int list = 0; - int o2o = 0; - int mbox_open_mode = 0; - int imap_flags = 0; - - pw = getpwuid (getuid ()); - - /* defaults */ - memset (&global, 0, sizeof (global)); - /* XXX the precedence is borked: - it's defaults < cmdline < file instead of defaults < file < cmdline */ - global.port = 143; - global.box = "INBOX"; - global.folder = ""; - global.user = strdup (pw->pw_name); - global.maildir = strdup (pw->pw_dir); - global.use_namespace = 1; -#if HAVE_LIBSSL - /* this will probably annoy people, but its the best default just in - * case people forget to turn it on - */ - global.require_ssl = 1; - global.use_tlsv1 = 1; -#endif - -#define FLAGS "alCLRc:defhp:qu:r:F:M:1I:s:vV" - -#if HAVE_GETOPT_LONG - while ((i = getopt_long (argc, argv, FLAGS, Opts, NULL)) != -1) -#else - while ((i = getopt (argc, argv, FLAGS)) != -1) -#endif - { - switch (i) - { - case 'l': - list = 1; - /* plopp */ - case 'a': - all = 1; - break; - case '1': - o2o = 1; - break; - case 'C': - mbox_open_mode |= OPEN_CREATE; - imap_flags |= IMAP_CREATE; - break; - case 'L': - mbox_open_mode |= OPEN_CREATE; - break; - case 'R': - imap_flags |= IMAP_CREATE; - break; - case 'c': - config = optarg; - break; - case 'd': - delete = 1; - break; - case 'e': - expunge = 1; - break; - case 'f': - mbox_open_mode |= OPEN_FAST; - fast = 1; - break; - case 'p': - global.port = atoi (optarg); - break; - case 'q': - Quiet++; - Verbose = 0; - break; - case 'r': - global.box = optarg; - break; - case 'F': - global.folder = optarg; - break; - case 'M': - global.maildir = optarg; - break; - case 'I': - global.inbox = optarg; - break; - case 's': -#if HAVE_LIBSSL - if (!strncasecmp ("imaps:", optarg, 6)) - { - global.use_imaps = 1; - global.port = 993; - global.use_sslv2 = 1; - global.use_sslv3 = 1; - optarg += 6; + string_list_t *nboxes = 0, *cpat; + const char *ps; + int not, fnot; + + for (; boxes; boxes = boxes->next) { + fnot = 1; + for (cpat = patterns; cpat; cpat = cpat->next) { + ps = cpat->string; + if (*ps == '!') { + ps++; + not = 1; + } else + not = 0; + if (matches( boxes->string, ps )) { + fnot = not; + break; + } } -#endif - global.host = optarg; - break; - case 'u': - global.user = optarg; - break; - case 'V': - Verbose = 1; - break; - case 'v': - version (); - case 'h': - usage (0); - default: - usage (1); - } - } - - if (!argv[optind] && !all) - { - fprintf (stderr, "No mailbox specified. Try isync -h\n"); - return 1; - } - - gethostname (Hostname, sizeof (Hostname)); - - load_config (config, &o2o); - - if (all && o2o) - { - DIR *dir; - struct dirent *de; - - if (global.inbox) { - boxes = malloc (sizeof (config_t)); - memcpy (boxes, &global, sizeof (config_t)); - boxes->box = "INBOX"; - boxes->path = global.inbox; + if (!fnot) + add_string_list( &nboxes, boxes->string ); } + return nboxes; +} - if (!(dir = opendir (global.maildir))) { - fprintf (stderr, "%s: %s\n", global.maildir, strerror(errno)); - return 1; +static void +merge_actions( channel_conf_t *chan, int mops, int sops, int have, int mask, int def ) +{ + if (mops & have) { + chan->mops &= ~mask; + chan->mops |= mops & mask; + chan->sops &= ~mask; + chan->sops |= sops & mask; + } else if (!(chan->mops & have)) { + if (global_mops & have) { + chan->mops |= global_mops & mask; + chan->sops |= global_sops & mask; + } else { + chan->mops |= def; + chan->sops |= def; + } } - while ((de = readdir (dir))) { - struct stat st; - char buf[PATH_MAX]; +} - if (*de->d_name == '.') - continue; - if (global.inbox && !strcmp (global.inbox, de->d_name)) - continue; - snprintf (buf, sizeof(buf), "%s/%s/cur", global.maildir, de->d_name); - if (stat (buf, &st) || !S_ISDIR (st.st_mode)) - continue; - box = malloc (sizeof (config_t)); - memcpy (box, &global, sizeof (config_t)); - box->path = strdup (de->d_name); - box->box = box->path; - box->next = boxes; - boxes = box; +int +main( int argc, char **argv ) +{ + channel_conf_t *chan; + store_conf_t *mconf, *sconf; + group_conf_t *group; + driver_t *mdriver, *sdriver; + store_t *mctx, *sctx; + string_list_t *umboxes, *usboxes, *mboxes, *sboxes, *mbox, *sbox, **mboxp, **sboxp, *cboxes, *chanptr; + char *config = 0, *channame, *boxlist, *opt, *ochar; + int all = 0, list = 0, cops = 0, mops = 0, sops = 0, gumboxes, gusboxes; + int oind, ret, op, multiple, pseudo = 0; + + gethostname( Hostname, sizeof(Hostname) ); + if ((ochar = strchr( Hostname, '.' ))) + *ochar = 0; + Pid = getpid(); + if (!(Home = getenv("HOME"))) { + fputs( "Fatal: $HOME not set\n", stderr ); + return 1; } - closedir (dir); - - imap = imap_connect (&global); - if (!imap) - return 1; - if (imap_list (imap)) - return 1; - } - if (list) - { - for (box = boxes; box; box = box->next) - puts (box->path); - return 0; - } - ret = 0; - for (box = boxes; (all && box) || (!all && argv[optind]); optind++) - { - if (!all) - { - if (o2o || NULL == (box = find_box (argv[optind]))) - { - /* if enough info is given on the command line, don't worry if - * the mailbox isn't defined. - */ - if (!global.host) - { - fprintf (stderr, "%s: no such mailbox\n", argv[optind]); - /* continue is ok here because we are not handling the - * `all' case. - */ - continue; + arc4_init(); + + for (oind = 1, ochar = 0; oind < argc; ) { + if (!ochar || !*ochar) { + if (argv[oind][0] != '-') + break; + if (argv[oind][1] == '-') { + opt = argv[oind++] + 2; + if (!*opt) + break; + if (!strcmp( opt, "config" )) { + if (oind >= argc) { + fprintf( stderr, "--config requires an argument.\n" ); + return 1; + } + config = argv[oind++]; + } else if (!memcmp( opt, "config=", 7 )) + config = opt + 7; + else if (!strcmp( opt, "all" )) + all = 1; + else if (!strcmp( opt, "list" )) + list = 1; + else if (!strcmp( opt, "help" )) + usage( 0 ); + else if (!strcmp( opt, "version" )) + version(); + else if (!strcmp( opt, "quiet" )) + Quiet++; + else if (!strcmp( opt, "verbose" )) { + Verbose = 1; + if (!Quiet) + Quiet = 1; + } else if (!strcmp( opt, "debug" )) { + Debug = 1; + if (!Quiet) + Quiet = 1; + } else if (!strcmp( opt, "pull" )) + cops |= XOP_PULL, mops |= XOP_HAVE_TYPE; + else if (!strcmp( opt, "push" )) + cops |= XOP_PUSH, mops |= XOP_HAVE_TYPE; + else if (!memcmp( opt, "create", 6 )) { + opt += 6; + op = OP_CREATE|XOP_HAVE_CREATE; + lcop: + if (!*opt) + cops |= op; + else if (!strcmp( opt, "-master" )) + mops |= op, ochar++; + else if (!strcmp( opt, "-slave" )) + sops |= op, ochar++; + else + goto badopt; + mops |= op & (XOP_HAVE_CREATE|XOP_HAVE_EXPUNGE); + } else if (!memcmp( opt, "expunge", 7 )) { + opt += 7; + op = OP_EXPUNGE|XOP_HAVE_EXPUNGE; + goto lcop; + } else if (!strcmp( opt, "no-expunge" )) + mops |= XOP_HAVE_EXPUNGE; + else if (!strcmp( opt, "no-create" )) + mops |= XOP_HAVE_CREATE; + else if (!strcmp( opt, "full" )) + mops |= XOP_HAVE_TYPE|XOP_PULL|XOP_PUSH; + else if (!strcmp( opt, "noop" )) + mops |= XOP_HAVE_TYPE; + else if (!memcmp( opt, "pull", 4 )) { + op = XOP_PULL; + lcac: + opt += 4; + if (!*opt) + cops |= op; + else if (*opt == '-') { + opt++; + goto rlcac; + } else + goto badopt; + } else if (!memcmp( opt, "push", 4 )) { + op = XOP_PUSH; + goto lcac; + } else { + op = 0; + rlcac: + if (!strcmp( opt, "new" )) + op |= OP_NEW; + else if (!strcmp( opt, "renew" )) + op |= OP_RENEW; + else if (!strcmp( opt, "delete" )) + op |= OP_DELETE; + else if (!strcmp( opt, "flags" )) + op |= OP_FLAGS; + else { + badopt: + fprintf( stderr, "Unknown option '%s'\n", argv[oind - 1] ); + return 1; + } + switch (op & XOP_MASK_DIR) { + case XOP_PULL: sops |= op & OP_MASK_TYPE; break; + case XOP_PUSH: mops |= op & OP_MASK_TYPE; break; + default: cops |= op; break; + } + mops |= XOP_HAVE_TYPE; + } + continue; + } + ochar = argv[oind++] + 1; + if (!*ochar) { + fprintf( stderr, "Invalid option '-'\n" ); + return 1; + } + } + switch (*ochar++) { + case 'a': + all = 1; + break; + case 'l': + list = 1; + break; + case 'c': + if (*ochar == 'T') { + ochar++; + pseudo = 1; + } + if (oind >= argc) { + fprintf( stderr, "-c requires an argument.\n" ); + return 1; + } + config = argv[oind++]; + break; + case 'C': + op = OP_CREATE|XOP_HAVE_CREATE; + cop: + if (*ochar == 'm') + mops |= op, ochar++; + else if (*ochar == 's') + sops |= op, ochar++; + else if (*ochar == '-') + ochar++; + else + cops |= op; + mops |= op & (XOP_HAVE_CREATE|XOP_HAVE_EXPUNGE); + break; + case 'X': + op = OP_EXPUNGE|XOP_HAVE_EXPUNGE; + goto cop; + case 'F': + cops |= XOP_PULL|XOP_PUSH; + case '0': + mops |= XOP_HAVE_TYPE; + break; + case 'n': + case 'd': + case 'f': + case 'N': + --ochar; + op = 0; + cac: + for (;; ochar++) { + if (*ochar == 'n') + op |= OP_NEW; + else if (*ochar == 'd') + op |= OP_DELETE; + else if (*ochar == 'f') + op |= OP_FLAGS; + else if (*ochar == 'N') + op |= OP_RENEW; + else + break; + } + if (op & OP_MASK_TYPE) + switch (op & XOP_MASK_DIR) { + case XOP_PULL: sops |= op & OP_MASK_TYPE; break; + case XOP_PUSH: mops |= op & OP_MASK_TYPE; break; + default: cops |= op; break; + } + else + cops |= op; + mops |= XOP_HAVE_TYPE; + break; + case 'L': + op = XOP_PULL; + goto cac; + case 'H': + op = XOP_PUSH; + goto cac; + case 'q': + Quiet++; + break; + case 'V': + Verbose = 1; + if (!Quiet) + Quiet = 1; + break; + case 'D': + Debug = 1; + if (!Quiet) + Quiet = 1; + break; + case 'v': + version(); + case 'h': + usage( 0 ); + default: + fprintf( stderr, "Unknown option '-%c'\n", *(ochar - 1) ); + return 1; } - global.path = argv[optind]; - box = &global; - if (o2o) - global.box = - (global.inbox && !strcmp (global.path, global.inbox)) ? - "INBOX" : global.path; - } } - do { - info ("Mailbox %s\n", box->path); - mail = maildir_open (box->path, mbox_open_mode); - if (!mail) - { - fprintf (stderr, "%s: unable to open mailbox\n", box->path); - ret = 1; - break; - } + if (merge_ops( cops, &mops, &sops )) + return 1; - if (box->max_size) - imap_flags |= IMAP_GET_SIZE; - imap = imap_open (box, fast ? mail->maxuid + 1 : 1, imap, imap_flags); - if (!imap) - { - fprintf (stderr, "%s: skipping mailbox due to IMAP error\n", - box->path); - ret = 1; - break; - } + if (load_config( config, pseudo )) + return 1; - info ("Synchronizing\n"); - i = (delete || box->delete) ? SYNC_DELETE : 0; - i |= (expunge || box->expunge) ? SYNC_EXPUNGE : 0; - if (sync_mailbox (mail, imap, i, box->max_size, box->max_messages)) - { - imap_close (imap); /* Just to be safe. Don't really know - * what the problem was. - */ - imap = NULL; /* context no longer valid */ - ret = 1; - break; - } + if (!all && !argv[oind]) { + fputs( "No channel specified. Try '" EXE " -h'\n", stderr ); + return 1; + } + if (!channels) { + fputs( "No channels defined. Try 'man " EXE "'\n", stderr ); + return 1; + } - if (!fast) - { - if ((expunge || box->expunge) && - (imap->deleted || mail->deleted)) - { - /* remove messages marked for deletion */ - info ("Expunging %d messages from server\n", imap->deleted); - if (imap_expunge (imap)) - { - imap_close (imap); - imap = NULL; + ret = 0; + chan = channels; + chanptr = 0; + mctx = sctx = 0; + mconf = sconf = 0; /* make-gcc-happy */ + mdriver = sdriver = 0; /* make-gcc-happy */ + gumboxes = gusboxes = 0; + umboxes = usboxes = 0; + if (all) + multiple = channels->next != 0; + else if (argv[oind + 1]) + multiple = 1; + else { + multiple = 0; + for (group = groups; group; group = group->next) + if (!strcmp( group->name, argv[oind] )) { + multiple = 1; + break; + } + } + for (;;) { + boxlist = 0; + if (!all) { + if (chanptr) + channame = chanptr->string; + else { + for (group = groups; group; group = group->next) + if (!strcmp( group->name, argv[oind] )) { + chanptr = group->channels; + channame = chanptr->string; + goto gotgrp; + } + channame = argv[oind]; + gotgrp: ; + } + if ((boxlist = strchr( channame, ':' ))) + *boxlist++ = 0; + for (chan = channels; chan; chan = chan->next) + if (!strcmp( chan->name, channame )) + goto gotchan; + fprintf( stderr, "No channel or group named '%s' defined.\n", channame ); ret = 1; - break; - } - info ("Expunging %d messages from local mailbox\n", - mail->deleted); - if (maildir_expunge (mail, 0)) { + goto gotnone; + gotchan: ; + } + merge_actions( chan, mops, sops, XOP_HAVE_TYPE, OP_MASK_TYPE, OP_MASK_TYPE ); + merge_actions( chan, mops, sops, XOP_HAVE_CREATE, OP_CREATE, 0 ); + merge_actions( chan, mops, sops, XOP_HAVE_EXPUNGE, OP_EXPUNGE, 0 ); + + mboxes = sboxes = cboxes = 0; + /* possible todo: handle master <-> slave swaps */ + if (mctx) { + if (mconf == chan->master) + goto gotmctx; + free_string_list( umboxes ); + umboxes = 0; + gumboxes = 0; + if (mconf->driver != chan->master->driver) { + mdriver->close_store( mctx ); + mctx = 0; + } + } + mconf = chan->master; + mdriver = mconf->driver; + if (!(mctx = mdriver->open_store( chan->master, mctx ))) { ret = 1; - break; - } + goto next; } - /* 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) { - if (maildir_expunge (mail, 1)) { + gotmctx: + if (sctx) { + if (sconf == chan->slave) + goto gotsctx; + free_string_list( usboxes ); + usboxes = 0; + gusboxes = 0; + if (sconf->driver != chan->slave->driver) { + sdriver->close_store( sctx ); + sctx = 0; + } + } + sconf = chan->slave; + sdriver = sconf->driver; + if (!(sctx = sdriver->open_store( chan->slave, sctx ))) { ret = 1; - break; - } + goto next; } - } - - } while (0); - - /* we never sync the same mailbox twice, so close it now */ - if (mail) - maildir_close (mail); - - /* the imap connection is not closed so we can keep the connection - * open, and there is no IMAP command for un-SELECT-ing a mailbox. - */ - if (all) - box = box->next; - } - /* gracefully close connection to the IMAP server */ - imap_close (imap); - return ret; + gotsctx: + info( "Channel %s\n", chan->name ); + if (list && multiple) + printf( "%s:\n", chan->name ); + if (boxlist) { + for (boxlist = strtok( boxlist, ",\n" ); boxlist; boxlist = strtok( 0, ",\n" )) + if (list) + puts( boxlist ); + else + switch (sync_boxes( mctx, boxlist, sctx, boxlist, chan )) { + case SYNC_MASTER_BAD: goto screwm; + case SYNC_SLAVE_BAD: goto screws; + case SYNC_FAIL: ret = 1; + } + } else if (chan->patterns) { + if (!gumboxes) { + if (mdriver->list( mctx, &umboxes ) != DRV_OK) { + screwm: + mdriver->close_store( mctx ); + free_string_list( umboxes ); + umboxes = 0; + gumboxes = 0; + mctx = 0; + ret = 1; + goto next; + } else { + gumboxes = 1; + if (mctx->conf->map_inbox) + add_string_list( &umboxes, mctx->conf->map_inbox ); + } + } + if (!gusboxes) { + if (sdriver->list( sctx, &usboxes ) != DRV_OK) { + screws: + sdriver->close_store( sctx ); + free_string_list( usboxes ); + usboxes = 0; + gusboxes = 0; + sctx = 0; + ret = 1; + goto next; + } else { + gusboxes = 1; + if (sctx->conf->map_inbox) + add_string_list( &usboxes, sctx->conf->map_inbox ); + } + } + mboxes = filter_boxes( umboxes, chan->patterns ); + sboxes = filter_boxes( usboxes, chan->patterns ); + for (mboxp = &mboxes; (mbox = *mboxp); ) { + for (sboxp = &sboxes; (sbox = *sboxp); sboxp = &sbox->next) + if (!strcmp( sbox->string, mbox->string )) { + *sboxp = sbox->next; + free( sbox ); + *mboxp = mbox->next; + mbox->next = cboxes; + cboxes = mbox; + goto gotdupe; + } + mboxp = &mbox->next; + gotdupe: ; + } + for (mbox = cboxes; mbox; mbox = mbox->next) + if (list) + puts( mbox->string ); + else + switch (sync_boxes( mctx, mbox->string, sctx, mbox->string, chan )) { + case SYNC_MASTER_BAD: goto screwm; + case SYNC_SLAVE_BAD: goto screws; + case SYNC_FAIL: ret = 1; + } + if ((chan->sops & OP_MASK_TYPE) && (chan->sops & OP_CREATE)) { + for (mbox = mboxes; mbox; mbox = mbox->next) + if (list) + puts( mbox->string ); + else + switch (sync_boxes( mctx, mbox->string, sctx, mbox->string, chan )) { + case SYNC_MASTER_BAD: goto screwm; + case SYNC_SLAVE_BAD: goto screws; + case SYNC_FAIL: ret = 1; + } + } + if ((chan->mops & OP_MASK_TYPE) && (chan->mops & OP_CREATE)) { + for (mbox = sboxes; mbox; mbox = mbox->next) + if (list) + puts( mbox->string ); + else + switch (sync_boxes( mctx, mbox->string, sctx, mbox->string, chan )) { + case SYNC_MASTER_BAD: goto screwm; + case SYNC_SLAVE_BAD: goto screws; + case SYNC_FAIL: ret = 1; + } + } + } else + if (list) + printf( "%s <=> %s\n", chan->master_name, chan->slave_name ); + else + switch (sync_boxes( mctx, chan->master_name, sctx, chan->slave_name, chan )) { + case SYNC_MASTER_BAD: goto screwm; + case SYNC_SLAVE_BAD: goto screws; + case SYNC_FAIL: ret = 1; + } + + next: + free_string_list( cboxes ); + free_string_list( mboxes ); + free_string_list( sboxes ); + if (all) { + if (!(chan = chan->next)) + break; + } else { + if (chanptr && (chanptr = chanptr->next)) + continue; + gotnone: + if (!argv[++oind]) + break; + } + } + free_string_list( usboxes ); + if (sctx) + sdriver->close_store( sctx ); + free_string_list( umboxes ); + if (mctx) + mdriver->close_store( mctx ); + + return ret; } diff --git a/src/mbsync.1 b/src/mbsync.1 @@ -0,0 +1,471 @@ +.ig +\" mbsync - mailbox synchronizer +\" Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> +\" Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net> +\" Copyright (C) 2004 Theodore Y. Ts'o <tytso@mit.edu> +\" +\" 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 +\" +\" As a special exception, mbsync may be linked with the OpenSSL library, +\" despite that library's more restrictive license. +.. +.TH mbsync 1 "2004 Mar 27" +.. +.SH NAME +mbsync - synchronize IMAP4 and Maildir mailboxes +.. +.SH SYNOPSIS +\fBmbsync\fR [\fIoptions\fR ...] {{\fIchannel\fR[\fB:\fIbox\fR[{\fB,\fR|\fB\\n\fR}...]]|\fIgroup\fR} ...|\fB-a\fR} +.. +.SH DESCRIPTION +\fBmbsync\fR is a command line application which synchronizes mailboxes; +currently Maildir and IMAP4 mailboxes are supported. +New messages, message deletions and flag changes can be propagated both ways; +the operation set can be selected in a fine-grained manner. +.br +Synchronization is based on unique message identifiers (UIDs), so no +identification conflicts can occur (as opposed to some other mail synchronizers). +OTOH, \fBmbsync\fR is susceptible to UID validity changes (that \fIshould\fR +never happen, but see "Compatibility" in the README). +Synchronization state is kept in one local text file per mailbox pair; +multiple replicas of a mailbox can be maintained. +.. +.SH OPTIONS +.TP +\fB-c\fR, \fB--config\fR \fIfile\fR +Read configuration from \fIfile\fR. +By default, the configuration is read from ~/.mbsyncrc. +.TP +\fB-a\fR, \fB--all\fR +Select all configured channels. Any channel/group specifications on the command +line are ignored. +.TP +\fB-l\fR, \fB--list\fR +Don't synchronize anything, but list all mailboxes in the selected channels +and exit. +.TP +\fB-C\fR[\fBm\fR][\fBs\fR], \fB--create\fR[\fB-master\fR|\fB-slave\fR] +Override any \fBCreate\fR options from the config file. See below. +.TP +\fB-X\fR[\fBm\fR][\fBs\fR], \fB--expunge\fR[\fB-master\fR|\fB-slave\fR] +Override any \fBExpunge\fR options from the config file. See below. +.TP +{\fB-n\fR|\fB-N\fR|\fB-d\fR|\fB-f\fR|\fB-0\fR|\fB-F\fR},\ + {\fB--new\fR|\fB--renew\fR|\fB--delete\fR|\fB--flags\fR|\fB--noop\fR|\fB--full\fR} +.TP +\r{\fB-L\fR|\fB-H\fR}[\fBn\fR][\fBN\fR][\fBd\fR][\fBf\fR],\ + {\fB--pull\fR|\fB--push\fR}[\fB-new\fR|\fB-renew\fR|\fB-delete\fR|\fB-flags\fR] +Override any \fBSync\fR options from the config file. See below. +.TP +\fB-h\fR, \fB--help\fR +Display a summary of command line options. +.TP +\fB-v\fR, \fB--version\fR +Display version information. +.TP +\fB-V\fR, \fB--verbose\fR +Enable \fIverbose\fR mode, which displays the IMAP4 network traffic. +.TP +\fB-D\fR, \fB--debug\fR +Enable printing \fIdebug\fR information. +.TP +\fB-q\fR, \fB--quiet\fR +Suppress informational messages. +If specified twice, suppress warning messages as well. +.. +.SH CONFIGURATION +The configuration file is mandatory; \fBmbsync\fR will not run without it. +Lines starting with a hash mark (\fB#\fR) are comments and are ignored entirely. +Configuration items are keywords followed by one or more arguments; +arguments containing spaces must be enclosed in double quotes (\fB"\fR). +All keywords (including those used as arguments) are case-insensitive. +There are a few global options, the rest applies to particular sections. +Sections are started by a section keyword and are terminated by an empty line +or end of file. +Every section defines an object with a an identifier unique within that +object class. +.P +There are two basic object classes: Stores and Channels. A Store defines +a collection of mailboxes; basically a folder, either local or remote. +A Channel connects two Stores, describing the way the two are synchronized. +.br +There are two auxiliary object classes: Accounts and Groups. An Account +describes the connection part of remote Stores, so a server connection can be +shared between multiple Stores. A Group aggregates multiple Channels to +save typing on the command line. +.. +.SS All Stores +These options can be used in all supported Store types. +.br +In this context, the term "remote" describes the second Store within a Channel, +and not necessarily a remote server. +.br +The special mailbox \fBINBOX\fR exists in every Store; its physical location +in the file system is Store type specific. +.. +.TP +\fBPath\fR \fIpath\fR +The location of the Store in the (server's) file system. +If this is no absolute path, the reference point is Store type specific. +This string is prepended to the mailbox names addressed in this Store, +but is not considered part of them; this is important for \fBPatterns\fR +in the Channels section. +Note that you \fBmust\fR append a slash if you want to specify an entire +directory. +(Default: \fI""\fR) +.. +.TP +\fBMaxSize\fR \fIsize\fR[\fBk\fR|\fBm\fR][\fBb\fR] +Messages larger than that will not be propagated into this Store. +This is useful for weeding out messages with large attachments. +\fBK\fR and \fBM\fR can be appended to the size to specify KiBytes resp. +MeBytes instead of bytes. \fBB\fR is accepted but superflous. +If \fIsize\fR is 0, the maximum message size is \fBunlimited\fR. +(Default: \fI0\fR) +.. +.TP +\fBMapInbox\fR \fImailbox\fR +Create a virtual mailbox (relative to \fBPath\fR), which is backed by +the \fBINBOX\fR. Makes sense in conjunction with \fBPatterns\fR in the +Channels section. +.. +.TP +\fBTrash\fR \fImailbox\fR +Specifies a mailbox (relative to \fBPath\fR) to copy deleted messages to +prior to expunging. See \fBINHERENT PROBLEMS\fR below. +(Default: none) +.. +.TP +\fBTrashNewOnly\fR \fIyes\fR|\fIno\fR +When trashing, copy only not yet propagated messages. This makes sense if the +remote Store has a \fBTrash\fR as well (with \fBTrashNewOnly\fR \fIno\fR). +(Default: \fIno\fR) +.. +.TP +\fBTrashRemoteNew\fR \fIyes\fR|\fIno\fR +When expunging the remote Store, copy not yet propagated messages to this +Store's \fBTrash\fR. When using this, the remote Store does not need an own +\fBTrash\fR at all, yet all messages are archived. +(Default: \fIno\fR) +.. +.SS Maildir Stores +The reference point for relative \fBPath\fRs is $HOME. +.P +As \fBmbsync\fR needs UIDs, but no standardized UID storage scheme exists for +Maildir, \fBmbsync\fR supports two schemes, each with its pros and cons. +.br +The \fBnative\fR scheme is stolen from the latest Maildir patches to \fBc-client\fR +and is therefore compatible with \fBpine\fR. The UID validity is stored in a +file named .uidvalidity; the UIDs are encoded in the file names of the messages. +.br +The \fBalternative\fR scheme is based on the UID mapping used by \fBisync\fR +versions 0.8 and 0.9.x. The invariant parts of the file names of the messages +are used as keys into a Berkeley database named .isyncuidmap.db, which holds +the UID validity as well. +.br +The \fBnative\fR scheme is faster, more space efficient, endianess independent +and "human readable", but will be disrupted if a message is copied from another +mailbox without getting a new file name; this would result in duplicated UIDs +sooner or later, which in turn results in a UID validity change, making +synchronization fail. +The \fBalternative\fR scheme would fail if a MUA changed a message's file name +in a part \fBmbsync\fR considers invariant; this would be interpreted as a +message deletion and a new message, resulting in unnecessary traffic. +.br +\fBMutt\fR is known to work fine with both schemes. +.br +Use \fBmdconvert\fR to convert mailboxes from one scheme to the other. +.. +.TP +\fBMaildirStore\fR \fIname\fR +Define the Maildir Store \fIname\fR, opening a section for its parameters. +.. +.TP +\fBAltMap\fR \fIyes\fR|\fIno\fR +Use the \fBalternative\fR UID storage scheme for mailboxes in this Store. +This does not affect mailboxes that do already have a UID storage scheme; +use \fBmdconvert\fR to change it. +(Default: \fIno\fR) +.. +.TP +\fBInbox\fR \fIpath\fR +The location of the \fBINBOX\fR. This is \fInot\fR relative to \fBPath\fR. +(Default: \fI~/Maildir\fR) +.. +.SS IMAP4 Accounts +.TP +\fBIMAPAccount\fR \fIname\fR +Define the IMAP4 Account \fIname\fR, opening a section for its parameters. +.. +.TP +\fBHost\fR [\fBimaps:\fR]\fIhost\fR +Specify the DNS name or IP address of the IMAP server. If \fIhost\fR is +prefixed with \fBimaps:\fR the connection is assumed to be an SSL connection +to port 993. +Note that modern servers support SSL on the default port 143 via the +STARTTLS extension, which will be used automatically by default. +.. +.TP +\fBPort\fR \fIport\fR +Specify the TCP port number of the IMAP server. (Default: 143 for imap, +993 for imaps) +.. +.TP +\fBUser\fR \fIusername\fR +Specify the login name on the IMAP server. (Default: current local user) +.. +.TP +\fBPass\fR \fIpassword\fR +Specify the password for \fIusername\fR on the IMAP server. +Note that this option is \fBNOT\fR required. +If no password is specified in the configuration file, \fBmbsync\fR +will prompt you for it. +.. +.TP +\fBTunnel\fR \fIcommand\fR +Specify a command to run to establish a connection rather than opening a TCP +socket. This allows you to run an IMAP session over an SSH tunnel, for +example. This makes most other IMAPAccount options superflous. +.. +.TP +\fBRequireCRAM\fR \fIyes\fR|\fIno\fR +If set to \fIyes\fR, \fBmbsync\fR will abort the connection if no CRAM-MD5 +authentication is possible. (Default: \fIno\fR) +.. +.TP +\fBRequireSSL\fR \fIyes\fR|\fIno\fR +\fBmbsync\fR will abort the connection if a TLS/SSL session cannot be +established with the IMAP server. (Default: \fIyes\fR) +.. +.TP +\fBCertificateFile\fR \fIpath\fR +File containing X.509 CA certificates used to verify server identities. +This option is \fImandatory\fR if SSL is used. See \fBSSL CERTIFICATES\fR below. +.. +.TP +\fBUseSSLv2\fR \fIyes\fR|\fIno\fR +Use SSLv2 for communication with the IMAP server over SSL? +(Default: \fIyes\fR if an imaps \fBHost\fR is used, otherwise \fIno\fR) +.. +.TP +\fBUseSSLv3\fR \fIyes\fR|\fIno\fR +Use SSLv3 for communication with the IMAP server over SSL? +(Default: \fIyes\fR if an imaps \fBHost\fR is used, otherwise \fIno\fR) +.. +.TP +\fBUseTLSv1\fR \fIyes\fR|\fIno\fR +Use TLSv1 for communication with the IMAP server over SSL? +(Default: \fIyes\fR) +.. +.SS IMAP Stores +The reference point for relative \fBPath\fRs is whatever the server likes it +to be; probably the user's $HOME or $HOME/Mail on that server. The location +of \fBINBOX\fR is up to the server as well and is usually irrelevant. +.TP +\fBIMAPStore\fR \fIname\fR +Define the IMAP4 Store \fIname\fR, opening a section for its parameters. +.. +.TP +\fBAccount\fR \fIaccount\fR +Specify which IMAP4 Account to use. Instead of defining an Account and +referencing it here, it is also possible to specify all the Account options +directly in the Store's section - this makes sense if an Account is used for +one Store only anyway. +.. +.TP +\fBUseNamespace\fR \fIyes\fR|\fIno\fR +Selects whether the server's first "personal" NAMESPACE should be prefixed to +mailbox names. Disabling this makes sense for some broken IMAP servers. +This option is meaningless if a \fBPath\fR was specified. +(Default: \fIyes\fR) +.. +.SS Channels +.TP +\fBChannel\fR \fIname\fR +Define the Channel \fIname\fR, opening a section for its parameters. +.. +.TP +{\fBMaster\fR|\fBSlave\fR} \fB:\fIstore\fB:\fR[\fImailbox\fR] +Specify the Master resp. Slave Store to be connected by this Channel. +If \fImailbox\fR is omitted, \fBINBOX\fR is assumed. +The \fImailbox\fR specification can be overridden by \fBPatterns\fR, which +in turn can be overridden by a mailbox list in a Channel reference (a Group +specification or the command line). +.. +.TP +\fBPattern\fR[\fBs\fR] [\fB!\fR]\fIpattern\fR ... +Instead of synchronizing only one mailbox pair, synchronize all mailboxes +that match the \fIpattern\fR(s). The mailbox names are the same on both +Master and Slave. Patterns are IMAP4 patterns, i.e., \fB*\fR matches anything +and \fB%\fR matches anything up to the next hierarchy delimiter. Prepending +\fB!\fR to a pattern makes it an exclusion. Multiple patterns can be specified +(either by supplying multiple arguments or by using \fBPattern\fR multiple +times); later matches take precedence. +Example: "\fBPatterns\fR\ \fI%\ !Trash\fR" +.. +.TP +\fBMaxSize\fR \fIsize\fR[\fBk\fR|\fBm\fR][\fBb\fR] +Analogous to the homonymous option in the Stores section, but applies equally +to Master and Slave. Note that this actually modifies the Stores, so take care +not to provide conflicting settings if you use the Stores in multiple Channels. +.. +.TP +\fBMaxMessages\fR \fIcount\fR +Sets the maximum number of messages to keep in each Slave mailbox. +This is useful for mailboxes where you keep a complete archive on the server, +but want to mirror only the last messages (for instance, for mailing lists). +The messages that were the first to arrive in the mailbox (independently of +the actual date of the message) will be deleted first. +Messages that are flagged (marked as important) and recent messages will not +be automatically deleted. +If \fIcount\fR is 0, the maximum number of messages is \fBunlimited\fR +(Default: \fI0\fR). +.. +.TP +\fBSync\fR {\fINone\fR|[\fIPull\fR] [\fIPush\fR] [\fINew\fR] [\fIReNew\fR] [\fIDelete\fR] [\fIFlags\fR]|\fIFull\fR} +Select the synchronization operation(s) to perform: +.br +\fBPull\fR - propagate changes from Master to Slave. +.br +\fBPush\fR - propagate changes from Slave to Master. +.br +\fBNew\fR - propagate newly appeared messages. +.br +\fBReNew\fR - previously refused messages are re-evaluated for propagation. +Useful after flagging affected messages in the source Store or enlarging +MaxSize in the destination Store. +.br +\fBDelete\fR - propagate message deletions. This applies only to messages that +are actually gone, i.e., were expunged. The affected messages in the remote +Store are marked as deleted only, i.e., they won't be really deleted until +that Store is expunged. +.br +\fBFlags\fR - propagate flag changes. Note that Deleted/Trashed is a flag as +well; this is particularily interesting if you use \fBmutt\fR with the +maildir_trash option. +.br +\fBAll\fR (\fB--full\fR on the command line) - all of the above. +This is the global default. +.br +\fBNone\fR (\fB--noop\fR on the command line) - don't propagate anything. +Useful if you want to expunge only. +.IP +\fBPull\fR and \fBPush\fR are direction flags, while \fBNew\fR, \fBReNew\fR, +\fBDelete\fR and \fBFlags\fR are type flags. The two flag classes make up a +two-dimensional matrix (a table). Its cells are the individual actions to +perform. There are two styles of asserting the cells: +.br +In the first style, the flags select entire rows/colums in the matrix. Only +the cells which are selected both horizontally and vertically are asserted. +Specifying no flags from a class is like specifying all flags from this class. +For example, "\fBSync\fR\ \fBPull\fR\ \fBNew\fR\ \fBFlags\fR" will propagate +new messages and flag changes from the Master to the Slave, +"\fBSync\fR\ \fBNew\fR\ \fBDelete\fR" will propagate message arrivals and +deletions both ways, and "\fBSync\fR\ \fBPush\fR" will propagate all changes +from the Slave to the Master. +.br +In the second style, direction flags are concatenated with type flags; every +compound flag immediately asserts a cell in the matrix. In addition to at least +one compound flag, the individual flags can be used as well, but as opposed to +the first style, they immediately assert all cells in their respective +row/column. For example, +"\fBSync\fR\ \fBPullNew\fR\ \fBPullDelete\fR\ \fBPush\fR" will propagate +message arrivals and deletions from the Master to the Slave and any changes +from the Slave to the Master. +Note that it is not allowed to assert a cell in two ways, e.g. +"\fBSync\fR\ \fBPullNew\fR\ \fBPull\fR" and +"\fBSync\fR\ \fBPullNew\fR\ \fBDelete\fR\ \fBPush\fR" induce error messages. +.. +.TP +\fBCreate\fR {\fINone\fR|\fIMaster\fR|\fISlave\fR|\fIBoth\fR} +Automatically create missing mailboxes [on the Master/Slave]. +Otherwise print an error message and skip that mailbox pair if a mailbox +does not exist. +(Global default: \fINone\fR) +.. +.TP +\fBExpunge\fR {\fINone\fR|\fIMaster\fR|\fISlave\fR|\fIBoth\fR} +Permanently remove all messages [on the Master/Slave] marked for deletion. +(Global default: \fINone\fR) +.. +.P +\fBSync\fR, \fBCreate\fR and \fBExpunge\fR can be used outside any section for +a global effect. The global settings are overridden by Channel-specific options, +which in turn are overridden by command line switches. +.. +.TP +\fBSyncState\fR {\fB*\fR|\fIpath\fR} +Set the location of this Channel's synchronization state files. \fB*\fR means +that the state should be saved in a file named .mbsyncstate in the +Slave mailbox itself; this has the advantage that you needn't to care for the +state file if you delete the mailbox, but it works only with Maildir mailboxes, +obviously. Otherwise this is interpreted as a string to prepend to the Slave +mailbox name to make up a complete path. +.br +This option can be used outside any section for a global effect. In this case +the appended string is made up according to the pattern +\fB:\fImaster\fB:\fImaster-box\fB_:\fIslave\fB:\fIslave-box\fR. +.br +(Global default: \fI~/.mbsync/\fR). +.. +.SS Groups +.TP +\fBGroup\fR \fIname\fR [\fIchannel\fR[\fB:\fIbox\fR[\fB,\fR...]]] ... +Define the Group \fIname\fR, opening a section for its parameters. +Note that even though Groups have an own namespace, they will "hide" Channels +with the same name on the command line. +.br +One or more Channels can be specified on the same line. +.br +If you supply one or more \fIbox\fRes to a \fIchannel\fR, they will be used +instead of what is specified in the Channel. The same can be done on the command +line, except that there newlines can be used as mailbox name separators as well. +.. +.TP +\fBChannel\fR[\fBs\fR] \fIchannel\fR[\fB:\fIbox\fR[\fB,\fR...]] ... +Add the specified channels to the group. This option can be specified multiple +times within a Group. +.. +.SH SSL CERTIFICATES +[to be done] +.. +.SH INHERENT PROBLEMS +Changes done after \fBmbsync\fR has retrieved the message list will not be +synchronised until the next time \fBmbsync\fR is invoked. +.P +Using \fBTrash\fR on IMAP Stores bears a race condition: messages will be +lost if they are marked as deleted after the message list was retrieved but +before the mailbox is expunged. There is no risk as long as the IMAP mailbox +is not simultaneously accessed by \fBmbsync\fR and another mail client. +.. +.SH FILES +.TP +.B ~/.mbsyncrc +Default configuration file +.TP +.B ~/.mbsync/ +Directory containing synchronization state files +.. +.SH SEE ALSO +mdconvert(1), isync(1), mutt(1), maildir(5) +.P +Up to date information on \fBmbsync\fR can be found at http://isync.sf.net/ +.. +.SH AUTHOR +Written by Michael R. Elkins <me@mutt.org>, +.br +rewritten and maintained by Oswald Buddenhagen <ossi@users.sf.net>, +.br +contributions by Theodore Y. Ts'o <tytso@mit.edu>. diff --git a/src/mbsyncrc.sample b/src/mbsyncrc.sample @@ -0,0 +1,82 @@ +# Global configuration section +# Values here are used as defaults for any following Channel section that +# doesn't specify them. +Expunge None +Create Both + +MaildirStore local +Path ~/Mail/ +Trash Trash + + +IMAPStore work +Host work.host.com +Pass xxxxxxxx +CertificateFile /etc/ssl/certs/ca-certificates.crt + +Channel work +Master :work: +Slave :local:work +Expunge Slave +Sync PullNew Push + + +IMAPStore personal +Host host.play.com +Port 6789 +RequireSSL no + +Channel personal +Master :personal: +Slave :local:personal +Expunge Both +MaxMessages 150 +MaxSize 200k + +IMAPStore remote +Tunnel "ssh -q host.remote.com /usr/sbin/imapd" + +Channel remote +Master :remote: +Slave :local:remote + + +Group boxes +Channels work personal remote + + +IMAPStore st1 +Host st1.domain.com +RequireCRAM yes +CertificateFile ~/.st1-certificate.crt + +IMAPStore st2 +Host imap.another-domain.com +Path non-standard/ +RequireSSL no +UseTLSv1 no + +Channel rst +Master :st1:somebox +Slave :st2: + + +IMAPAccount server +Host imaps:foo.bar.com +CertificateFile ~/.server-certificate.crt + +IMAPStore server +Account server +MapInbox inbox +Trash ~/trash +TrashRemoteNew yes + +MaildirStore mirror +Path ~/Maildir/ + +Channel o2o +Master :server: +Slave :mirror: +Patterns % + +Group partial o2o:inbox,sent-mail,foobar diff --git a/src/mdconvert.1 b/src/mdconvert.1 @@ -0,0 +1,51 @@ +.ig +\" mdconvert - Maildir mailbox UID storage scheme converter +\" Copyright (C) 2004 Oswald Buddenhagen <ossi@users.sf.net> +\" +\" 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 mdconvert 1 "2004 Mar 27" +.. +.SH NAME +mdconvert - Maildir mailbox UID storage scheme converter +.. +.SH SYNOPSIS +\fBmdconvert\fR [\fIoptions\fR ...] \fImailbox\fR ... +.. +.SH DESCRIPTION +\fBmdconvert\fR converts Maildir mailboxes between the two UID storage schemes +supported by \fBmbsync\fR. See \fBmbsync\fR's manual page for details on these +schemes. +.. +.SH OPTIONS +.TP +\fB-a\fR, \fB--alt\fR +Convert to the \fBalternative\fR (Berkeley DB based) UID storage scheme. +.TP +\fB-n\fR, \fB--native\fR +Convert to the \fBnative\fR (file name based) UID storage scheme. +This is the default. +.TP +\fB-h\fR, \fB--help\fR +Displays a summary of command line options. +.TP +\fB-v\fR, \fB--version\fR +Displays version information. +.. +.SH SEE ALSO +mbsync(1) +.. +.SH AUTHOR +Written and maintained by Oswald Buddenhagen <ossi@users.sf.net>. diff --git a/src/mdconvert.c b/src/mdconvert.c @@ -0,0 +1,256 @@ +/* + * mdconvert - Maildir UID scheme converter + * Copyright (C) 2004 Oswald Buddenhagen <ossi@users.sf.net> + * + * 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 <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <fcntl.h> +#include <sys/stat.h> +#include <stdarg.h> +#include <dirent.h> +#include <limits.h> +#include <errno.h> +#include <string.h> +#include <ctype.h> + +#include <db.h> + +#define EXE "mdconvert" + +static int +nfsnprintf( char *buf, int blen, const char *fmt, ... ) +{ + int ret; + va_list va; + + va_start( va, fmt ); + if (blen <= 0 || (unsigned)(ret = vsnprintf( buf, blen, fmt, va )) >= (unsigned)blen) { + fputs( "Fatal: buffer too small. Please report a bug.\n", stderr ); + abort(); + } + va_end( va ); + return ret; +} + +static const char *subdirs[] = { "cur", "new" }; +static struct flock lck; +static DBT key, value; + +static inline int +convert( const char *box, int altmap ) +{ + DB *db; + DIR *d; + struct dirent *e; + const char *u, *ru; + char *p, *s, *dpath, *spath, *dbpath; + int i, n, ret, sfd, dfd, bl, ml, uv[2], uid; + struct stat st; + char buf[_POSIX_PATH_MAX], buf2[_POSIX_PATH_MAX]; + char umpath[_POSIX_PATH_MAX], uvpath[_POSIX_PATH_MAX], tdpath[_POSIX_PATH_MAX]; + + if (stat( box, &st ) || !S_ISDIR(st.st_mode)) { + fprintf( stderr, "'%s' is no Maildir mailbox.\n", box ); + return 1; + } + + nfsnprintf( umpath, sizeof(umpath), "%s/.isyncuidmap.db", box ); + nfsnprintf( uvpath, sizeof(uvpath), "%s/.uidvalidity", box ); + if (altmap) + dpath = umpath, spath = uvpath, dbpath = tdpath; + else + spath = umpath, dpath = uvpath, dbpath = umpath; + nfsnprintf( tdpath, sizeof(tdpath), "%s.tmp", dpath ); + if ((sfd = open( spath, O_RDWR )) < 0) { + if (errno != ENOENT) + perror( spath ); + return 1; + } + if (fcntl( sfd, F_SETLKW, &lck )) { + perror( spath ); + goto sbork; + } + if ((dfd = open( tdpath, O_RDWR|O_CREAT, 0600 )) < 0) { + perror( tdpath ); + goto sbork; + } + if (db_create( &db, 0, 0 )) { + fputs( "Error: db_create() failed\n", stderr ); + goto tbork; + } + if ((ret = db->open( db, 0, dbpath, 0, DB_HASH, DB_CREATE, 0 ))) { + db->err( db, ret, "Error: db->open(%s)", dbpath ); + dbork: + db->close( db, 0 ); + tbork: + unlink( tdpath ); + close( dfd ); + sbork: + close( sfd ); + return 1; + } + key.data = (void *)"UIDVALIDITY"; + key.size = 11; + if (altmap) { + if ((n = read( sfd, buf, sizeof(buf) )) <= 0 || + (buf[n] = 0, sscanf( buf, "%d\n%d", &uv[0], &uv[1] ) != 2)) + { + fprintf( stderr, "Error: cannot read UIDVALIDITY of '%s'.\n", box ); + goto dbork; + } + value.data = uv; + value.size = sizeof(uv); + if ((ret = db->put( db, 0, &key, &value, 0 ))) { + db->err( db, ret, "Error: cannot write UIDVALIDITY for '%s'", box ); + goto dbork; + } + } else { + if ((ret = db->get( db, 0, &key, &value, 0 ))) { + db->err( db, ret, "Error: cannot read UIDVALIDITY of '%s'", box ); + goto dbork; + } + n = sprintf( buf, "%d\n%d\n", ((int *)value.data)[0], ((int *)value.data)[1] ); + if (write( dfd, buf, n ) != n) { + fprintf( stderr, "Error: cannot write UIDVALIDITY for '%s'.\n", box ); + goto dbork; + } + } + + again: + for (i = 0; i < 2; i++) { + bl = nfsnprintf( buf, sizeof(buf), "%s/%s/", box, subdirs[i] ); + if (!(d = opendir( buf ))) { + perror( "opendir" ); + goto dbork; + } + while ((e = readdir( d ))) { + if (*e->d_name == '.') + continue; + nfsnprintf( buf + bl, sizeof(buf) - bl, "%s", e->d_name ); + memcpy( buf2, buf, bl ); + p = strstr( e->d_name, ",U=" ); + if (p) + for (u = p, ru = p + 3; isdigit( (unsigned char)*ru ); ru++); + else + u = ru = strchr( e->d_name, ':' ); + if (u) + ml = u - e->d_name; + else + ru = "", ml = INT_MAX; + if (altmap) { + if (!p) + continue; + key.data = e->d_name; + key.size = (size_t)(strchr( e->d_name, ',' ) - e->d_name); + uid = atoi( p + 3 ); + value.data = &uid; + value.size = sizeof(uid); + if ((ret = db->put( db, 0, &key, &value, 0 ))) { + db->err( db, ret, "Error: cannot write UID for '%s'", box ); + goto ebork; + } + nfsnprintf( buf2 + bl, sizeof(buf2) - bl, "%.*s%s", ml, e->d_name, ru ); + } else { + s = strpbrk( e->d_name, ",:" ); + key.data = e->d_name; + key.size = s ? (size_t)(s - e->d_name) : strlen( e->d_name ); + if ((ret = db->get( db, 0, &key, &value, 0 ))) { + if (ret != DB_NOTFOUND) { + db->err( db, ret, "Error: cannot read UID for '%s'", box ); + goto ebork; + } + continue; + } + uid = *(int *)value.data; + nfsnprintf( buf2 + bl, sizeof(buf2) - bl, "%.*s,U=%d%s", ml, e->d_name, uid, ru ); + } + if (rename( buf, buf2 )) { + if (errno == ENOENT) + goto again; + perror( buf ); + ebork: + closedir( d ); + goto dbork; + } + + } + closedir( d ); + } + + db->close( db, 0 ); + close( dfd ); + if (rename( tdpath, dpath )) { + perror( dpath ); + return 1; + } + if (unlink( spath )) + perror( spath ); + close( sfd ); + return 0; +} + +int +main( int argc, char **argv ) +{ + int oint, ret, altmap = 0; + + for (oint = 1; oint < argc; oint++) { + if (!strcmp( argv[oint], "-h" ) || !strcmp( argv[oint], "--help" )) { + puts( +"Usage: " EXE " [-a] mailbox...\n" +" -a, --alt convert to alternative (DB based) UID scheme\n" +" -n, --native convert to native (file name based) UID scheme (default)\n" +" -h, --help show this help message\n" +" -v, --version display version" + ); + return 0; + } else if (!strcmp( argv[oint], "-v" ) || !strcmp( argv[oint], "--version" )) { + puts( EXE " " VERSION " - Maildir UID scheme converter" ); + return 0; + } else if (!strcmp( argv[oint], "-a" ) || !strcmp( argv[oint], "--alt" )) { + altmap = 1; + } else if (!strcmp( argv[oint], "-n" ) || !strcmp( argv[oint], "--native" )) { + altmap = 0; + } else if (!strcmp( argv[oint], "--" )) { + oint++; + break; + } else if (argv[oint][0] == '-') { + fprintf( stderr, "Unrecognized option '%s'. Try " EXE " -h\n", argv[oint] ); + return 1; + } else + break; + } + if (oint == argc) { + fprintf( stderr, "Mailbox specification missing. Try " EXE " -h\n" ); + return 1; + } +#if SEEK_SET != 0 + lck.l_whence = SEEK_SET; +#endif +#if F_WRLCK != 0 + lck.l_type = F_WRLCK; +#endif + ret = 0; + for (; oint < argc; oint++) + ret |= convert( argv[oint], altmap ); + return ret; +} + diff --git a/src/sync.c b/src/sync.c @@ -1,8 +1,7 @@ -/* $Id$ - * - * isync - IMAP4 to maildir mailbox synchronizer +/* + * mbsync - mailbox synchronizer * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> - * Copyright (C) 2002-2003 Oswald Buddenhagen <ossi@users.sf.net> + * Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net> * * 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 @@ -18,7 +17,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * - * As a special exception, isync may be linked with the OpenSSL library, + * As a special exception, mbsync may be linked with the OpenSSL library, * despite that library's more restrictive license. */ @@ -34,367 +33,940 @@ #include <errno.h> #include <sys/stat.h> -static unsigned int MaildirCount = 0; +static const char Flags[] = { 'D', 'F', 'R', 'S', 'T' }; -message_t * -find_msg (message_t * list, unsigned int uid) +static int +parse_flags( const char *buf ) { - for (; list; list = list->next) - if (list->uid == uid) - return list; - return 0; + unsigned flags, i, d; + + for (flags = i = d = 0; i < as(Flags); i++) + if (buf[d] == Flags[i]) { + flags |= (1 << i); + d++; + } + return flags; } static int -set_uid (DB * db, const char *f, unsigned int uid) +make_flags( int flags, char *buf ) { - char *s; - DBT key, val; - int ret; - - memset (&key, 0, sizeof(key)); - memset (&val, 0, sizeof(val)); - key.data = (void *) f; - s = strchr (f, ':'); - key.size = s ? (size_t) (s - f) : strlen (f); - val.data = (void *) &uid; - val.size = sizeof (uid); - ret = db->put (db, 0, &key, &val, 0); - if (ret < 0) - fprintf (stderr, "Unexpected error (%d) from db_put(%.*s, %d)\n", - ret, key.size, f, uid); - db->sync(db, 0); - return 0; + unsigned i, d; + + for (i = d = 0; i < as(Flags); i++) + if (flags & (1 << i)) + buf[d++] = Flags[i]; + buf[d] = 0; + return d; } -int -sync_mailbox (mailbox_t * mbox, imap_t * imap, int flags, - unsigned int max_size, unsigned int max_msgs) +static void +makeopts( int dops, store_conf_t *dconf, int *dopts, + store_conf_t *sconf, int *sopts ) { - message_t *cur; - message_t *tmp; - char path[_POSIX_PATH_MAX]; - char newpath[_POSIX_PATH_MAX]; - char suffix[_POSIX_PATH_MAX]; - char *p; - int fd; - int ret; - int fetched = 0; - int upload = 0; - unsigned int msg_count; - - if (mbox->uidseen) - { - if (mbox->uidvalidity != imap->uidvalidity) - { - /* if the UIDVALIDITY value has changed, it means all our - * local UIDs are invalid, so we can't sync. - */ - fprintf (stderr, - "ERROR: UIDVALIDITY of '%s' changed on server\n", - imap->box->box); - return -1; + if (dops & (OP_DELETE|OP_FLAGS)) { + *dopts |= OPEN_SETFLAGS; + *sopts |= OPEN_OLD; + if (dops & OP_FLAGS) + *sopts |= OPEN_FLAGS; + } + if (dops & (OP_NEW|OP_RENEW)) { + *dopts |= OPEN_APPEND; + if (dops & OP_RENEW) + *sopts |= OPEN_OLD; + if (dops & OP_NEW) + *sopts |= OPEN_NEW; + if (dops & OP_EXPUNGE) + *sopts |= OPEN_FLAGS; + if (dconf->max_size) + *sopts |= OPEN_SIZE; + } + if (dops & OP_EXPUNGE) { + *dopts |= OPEN_EXPUNGE; + if (dconf->trash) { + if (!dconf->trash_only_new) + *dopts |= OPEN_OLD; + *dopts |= OPEN_NEW|OPEN_FLAGS; + } else if (sconf->trash && sconf->trash_remote_new) + *dopts |= OPEN_NEW|OPEN_FLAGS; } - } - else if (maildir_set_uidvalidity (mbox, imap->uidvalidity)) - { - fputs ("ERROR: unable to store UIDVALIDITY\n", stderr); - return -1; - } - - if (mbox->maxuid == 0 || imap->maxuid > mbox->maxuid) - { - mbox->maxuid = imap->maxuid; - } - - /* if we are --fast mode, the mailbox wont have been loaded, so - * this next step is skipped. - */ - for (cur = mbox->msgs; cur; cur = cur->next) - { - tmp = find_msg (imap->msgs, cur->uid); - if (!tmp) - { - /* if this message wasn't fetched from the server, attempt to - * upload it - */ - if (cur->uid == (unsigned int) -1) - { - struct stat sb; - - if ((cur->flags & D_DELETED) && (flags & SYNC_EXPUNGE)) - { - /* - * This message is marked as deleted and we are - * expunging. Don't upload to the server. - */ - continue; + if (dops & OP_CREATE) + *dopts |= OPEN_CREATE; +} + +static void +dump_box( store_t *ctx ) +{ + message_t *msg; + char fbuf[16]; /* enlarge when support for keywords is added */ + + if (Debug) + for (msg = ctx->msgs; msg; msg = msg->next) { + make_flags( msg->flags, fbuf ); + printf( " message %d, %s, %d\n", msg->uid, fbuf, msg->size ); } +} - if (!upload) - info ("Uploading messages"); - infoc ('.'); - fflush (stdout); - upload++; - - /* upload the message if its not too big */ - snprintf (path, sizeof (path), "%s/%s/%s", mbox->path, - cur->new ? "new" : "cur", cur->file); - if (stat (path, &sb)) - { - perror (path); - continue; /* not fatal */ +static message_t * +findmsg( store_t *ctx, int uid, message_t **nmsg, const char *who ) +{ + message_t *msg; + + if (uid > 0) { + if (*nmsg && (*nmsg)->uid == uid) { + debug( " %s came in sequence\n", who ); + msg = *nmsg; + found: + *nmsg = msg->next; + if (!(msg->status & M_DEAD)) { + msg->status |= M_PROCESSED; + return msg; + } + debug( " ... but it vanished under our feet!\n" ); + } else { + for (msg = ctx->msgs; msg; msg = msg->next) + if (msg->uid == uid) { + debug( " %s came out of sequence\n", who ); + goto found; + } + debug( " %s not present\n", who ); } - if (imap->box->max_size > 0 - && sb.st_size > imap->box->max_size) - { - info ("Local message %s is too large (%lu), skipping...\n", - cur->file, (unsigned long) sb.st_size); - continue; + } else + debug( " no %s expected\n", who ); + return 0; +} + +#define S_DEAD (1<<0) +#define S_EXPIRED (1<<1) +#define S_DEL_MASTER (1<<2) +#define S_DEL_SLAVE (1<<3) +#define S_EXP_SLAVE (1<<4) + +typedef struct sync_rec { + struct sync_rec *next; + /* string_list_t *keywords; */ + int muid, suid; + unsigned char flags, status; +} sync_rec_t; + + +#define EX_OK 0 +#define EX_FAIL 1 +#define EX_STORE_BAD 2 +#define EX_RSTORE_BAD 3 + +static int +expunge( store_t *ctx, store_t *rctx ) +{ + driver_t *driver = ctx->conf->driver, *rdriver = rctx->conf->driver; + message_t *msg; + msg_data_t msgdata; + + for (msg = ctx->msgs; msg; msg = msg->next) + if (msg->flags & F_DELETED) { + if (ctx->conf->trash) { + if (!ctx->conf->trash_only_new || (msg->status & M_NOT_SYNCED)) { + debug( " trashing message %d\n", msg->uid ); + switch (driver->trash_msg( ctx, msg )) { + case DRV_STORE_BAD: return EX_STORE_BAD; + default: return EX_FAIL; + case DRV_OK: break; + } + } else + debug( " not trashing message %d - not new\n", msg->uid ); + } else if (rctx->conf->trash && rctx->conf->trash_remote_new) { + if (msg->status & M_NOT_SYNCED) { + if (!rctx->conf->max_size || msg->size <= rctx->conf->max_size) { + debug( " remote trashing message %d\n", msg->uid ); + msgdata.flags = msg->flags; + switch (driver->fetch_msg( ctx, msg, &msgdata )) { + case DRV_STORE_BAD: return EX_STORE_BAD; + default: return EX_FAIL; + case DRV_OK: break; + } + switch (rdriver->store_msg( rctx, &msgdata, 0 )) { + case DRV_STORE_BAD: return EX_RSTORE_BAD; + default: return EX_FAIL; + case DRV_OK: break; + } + } else + debug( " not remote trashing message %d - too big\n", msg->uid ); + } else + debug( " not remote trashing message %d - not new\n", msg->uid ); + } + } + + switch (driver->close( ctx )) { + case DRV_STORE_BAD: return EX_STORE_BAD; + default: return EX_FAIL; + case DRV_OK: return EX_OK;; + } +} + +/* cases: + a) both non-null + b) only master null + b.1) muid 0 + b.2) muid -1 + b.3) master not scanned + b.4) master gone + c) only slave null + c.1) suid 0 + c.2) suid -1 + c.3) slave not scanned + c.4) slave gone + d) both null + d.1) both gone + d.2) muid 0, slave not scanned + d.3) muid -1, slave not scanned + d.4) master gone, slave not scanned + d.5) muid 0, slave gone + d.6) muid -1, slave gone + d.7) suid 0, master not scanned + d.8) suid -1, master not scanned + d.9) slave gone, master not scanned + d.10) suid 0, master gone + d.11) suid -1, master gone + impossible cases: both muid & suid 0 or -1, both not scanned +*/ +static int +sync_old( int tops, store_t *sctx, store_t *tctx, store_conf_t *tconf, FILE *jfp, int pull, + unsigned char *nflags, sync_rec_t *srec, message_t *smsg, message_t *tmsg, int dels, int delt ) +{ + driver_t *tdriver = tctx->conf->driver, *sdriver = sctx->conf->driver; + int uid, tuid, unex; + unsigned char sflags, aflags, dflags, rflags; + msg_data_t msgdata; + + /* excludes (push) c.3) d.2) d.3) d.4) / (pull) b.3) d.7) d.8) d.9) */ + tuid = pull ? srec->suid : srec->muid; + if (!tuid) { + /* b.1) / c.1) */ + debug( pull ? " no more slave\n" : " no more master\n" ); + } else if (dels) { + /* c.4) d.9) / b.4) d.4) */ + debug( pull ? " master vanished\n" : " slave vanished\n" ); + if (tmsg && tmsg->flags != *nflags) + info( "Info: conflicting changes in (%d,%d)\n", srec->muid, srec->suid ); + if (tops & OP_DELETE) { + debug( pull ? " -> pulling delete\n" : " -> pushing delete\n" ); + switch (tdriver->set_flags( tctx, tmsg, tuid, F_DELETED, 0 )) { + case DRV_STORE_BAD: return pull ? SYNC_SLAVE_BAD : SYNC_MASTER_BAD; + case DRV_BOX_BAD: return SYNC_FAIL; + default: /* ok */ break; + case DRV_OK: + if (pull) { + fprintf( jfp, "< %d %d 0\n", srec->muid, srec->suid ); + srec->muid = 0; + } else { + fprintf( jfp, "> %d %d 0\n", srec->muid, srec->suid ); + srec->suid = 0; + } + } } - fd = open (path, O_RDONLY); - if (fd == -1) - { - /* This can happen if the message was simply deleted (ok) - or the flags changed (not ok - maildir sucks). */ - fprintf (stderr, "Error, unable to open %s: %s (errno %d)\n", - path, strerror (errno), errno); - continue; + } else if (!smsg) + /* c.1) c.2) d.7) d.8) / b.1) b.2) d.2) d.3) */ + ; + else if (tuid < 0) { + /* b.2) / c.2) */ + debug( pull ? " no slave yet\n" : " no master yet\n" ); + if (tops & OP_RENEW) { + if ((tops & OP_EXPUNGE) && (smsg->flags & F_DELETED)) { + debug( pull ? " -> not pulling - would be expunged anyway\n" : " -> not pushing - would be expunged anyway\n" ); + smsg->status |= M_NOT_SYNCED; + } else { + if ((smsg->flags & F_FLAGGED) || !tconf->max_size || smsg->size <= tconf->max_size) { + debug( pull ? " -> pulling it\n" : " -> pushing it\n" ); + msgdata.flags = smsg->flags; + switch (sdriver->fetch_msg( sctx, smsg, &msgdata )) { + case DRV_STORE_BAD: return pull ? SYNC_MASTER_BAD : SYNC_SLAVE_BAD; + case DRV_BOX_BAD: return SYNC_FAIL; + default: /* ok */ smsg->status |= M_NOT_SYNCED; break; + case DRV_OK: + smsg->flags = msgdata.flags; + switch (tdriver->store_msg( tctx, &msgdata, &uid )) { + case DRV_STORE_BAD: return pull ? SYNC_SLAVE_BAD : SYNC_MASTER_BAD; + default: return SYNC_FAIL; + case DRV_OK: + if (pull) { + srec->suid = uid; + fprintf( jfp, "> %d -1 %d\n", srec->muid, srec->suid ); + } else { + srec->muid = uid; + fprintf( jfp, "< -1 %d %d\n", srec->suid, srec->muid ); + } + *nflags = smsg->flags; + } + } + } else { + debug( pull ? " -> not pulling - still too big\n" : " -> not pushing - still too big\n" ); + smsg->status |= M_NOT_SYNCED; + } + } + } else + smsg->status |= M_NOT_SYNCED; + } else if (!delt) { + /* a) & b.3) / c.3) */ + debug( pull ? " may pull\n" : " may push\n" ); + if (tops & OP_FLAGS) { + debug( pull ? " -> pulling flags\n" : " -> pushing flags\n" ); + sflags = smsg->flags; + aflags = sflags & ~*nflags; + dflags = ~sflags & *nflags; + unex = 0; + if (srec->status & S_EXPIRED) { + if (!pull) { + if (sflags & F_DELETED) { + if (!(sflags & F_FLAGGED)) + aflags &= ~F_DELETED; + } else + unex = 1; + } else { + if ((sflags & F_FLAGGED) && !(sflags & F_DELETED)) { + unex = 1; + dflags |= F_DELETED; + } + } + } + rflags = (*nflags | aflags) & ~dflags; + if ((tops & OP_EXPUNGE) && (rflags & F_DELETED) && + (!tctx->conf->trash || tctx->conf->trash_only_new)) + { + aflags &= F_DELETED; + dflags = 0; + } + switch (tdriver->set_flags( tctx, tmsg, tuid, aflags, dflags )) { + case DRV_STORE_BAD: return pull ? SYNC_SLAVE_BAD : SYNC_MASTER_BAD; + case DRV_BOX_BAD: return SYNC_FAIL; + default: /* ok */ break; + case DRV_OK: + *nflags = rflags; + if (unex) { + debug( "unexpiring pair(%d,%d)\n", srec->muid, srec->suid ); + /* log last, so deletion can't be misinterpreted! */ + fprintf( jfp, "~ %d %d 0\n", srec->muid, srec->suid ); + srec->status &= ~S_EXPIRED; + } + } } + } /* else b.4) / c.4) */ + return SYNC_OK; +} - cur->size = sb.st_size; - cur->uid = imap_append_message (imap, fd, cur); - if (cur->uid != (unsigned int) -1) { - /* update the db */ - set_uid (mbox->db, cur->file, cur->uid); - if (!cur->uid) - warn ("Warning: no UID for new messge %s\n", cur->file); - else if (cur->uid > mbox->maxuid) - mbox->maxuid = cur->uid; +static int +sync_new( int tops, store_t *sctx, store_t *tctx, store_conf_t *tconf, FILE *jfp, sync_rec_t ***srecadd, int pull, int *smaxuid ) +{ + driver_t *tdriver = tctx->conf->driver, *sdriver = sctx->conf->driver; + sync_rec_t *srec; + message_t *msg; + int nmsgs, uid; + msg_data_t msgdata; + + for (nmsgs = 0, msg = sctx->msgs; msg; msg = msg->next) + if (!(msg->status & M_PROCESSED)) { + if (tops & OP_NEW) { + debug( pull ? "new message %d on master\n" : "new message %d on slave\n", msg->uid ); + if ((tops & OP_EXPUNGE) && (msg->flags & F_DELETED)) { + debug( pull ? " not pulling - would be expunged anyway\n" : " not pushing - would be expunged anyway\n" ); + msg->status |= M_NOT_SYNCED; + } else { + if ((msg->flags & F_FLAGGED) || !tconf->max_size || msg->size <= tconf->max_size) { + debug( pull ? " pulling it\n" : " pushing it\n" ); + if (!nmsgs) + info( pull ? "Pulling new messages..." : "Pushing new messages..." ); + else + infoc( '.' ); + nmsgs++; + msgdata.flags = msg->flags; + switch (sdriver->fetch_msg( sctx, msg, &msgdata )) { + case DRV_STORE_BAD: return pull ? SYNC_MASTER_BAD : SYNC_SLAVE_BAD; + case DRV_BOX_BAD: return SYNC_FAIL; + case DRV_MSG_BAD: /* ok */ msg->status |= M_NOT_SYNCED; continue; + } + msg->flags = msgdata.flags; + switch (tdriver->store_msg( tctx, &msgdata, &uid )) { + case DRV_STORE_BAD: return pull ? SYNC_SLAVE_BAD : SYNC_MASTER_BAD; + default: return SYNC_FAIL; + case DRV_OK: break; + } + } else { + debug( pull ? " not pulling - too big\n" : " not pushing - too big\n" ); + msg->status |= M_NOT_SYNCED; + uid = -1; + } + srec = nfmalloc( sizeof(*srec) ); + if (pull) { + srec->muid = msg->uid; + srec->suid = uid; + } else { + srec->muid = uid; + srec->suid = msg->uid; + } + srec->flags = msg->flags; + srec->status = 0; + srec->next = 0; + **srecadd = srec; + *srecadd = &srec->next; + fprintf( jfp, "+ %d %d %u\n", srec->muid, srec->suid, srec->flags ); + if (*smaxuid < msg->uid) { + *smaxuid = msg->uid; + fprintf( jfp, pull ? "( %d\n" : ") %d\n", msg->uid ); + } + } + } else + msg->status |= M_NOT_SYNCED; } + if (nmsgs) + info( " %d messages\n", nmsgs ); + return SYNC_OK; +} - close (fd); - } - /* - * message used to exist on server but no longer does (we know - * this beacause it has a UID associated with it). - */ - else if (flags & SYNC_DELETE) - { - cur->flags |= D_DELETED; - cur->dead = 1; - mbox->deleted++; - } - /* if the user doesn't want local msgs deleted when they don't - * exist on the server, warn that such messages exist. - */ - else - info ("Local message %u doesn't exist on server\n", cur->uid); - continue; +static char * +clean_strdup( const char *s ) +{ + char *cs; + int i; + + cs = nfstrdup( s ); + for (i = 0; cs[i]; i++) + if (cs[i] == '/') + cs[i] = '!'; + return cs; +} + +int +sync_boxes( store_t *mctx, const char *mname, + store_t *sctx, const char *sname, + channel_conf_t *chan ) +{ + driver_t *mdriver = mctx->conf->driver, *sdriver = sctx->conf->driver; + message_t *mmsg, *smsg, *nmmsg, *nsmsg; + sync_rec_t *recs, *srec, **srecadd, *nsrec; + char *dname, *jname, *nname, *lname, *s, *cmname, *csname; + FILE *dfp, *jfp, *nfp; + int mopts, sopts; + int nom, nos, delm, dels, mex, sex; + int muidval, suidval, smaxxuid, mmaxuid, smaxuid, minwuid, maxwuid; + int t1, t2, t3; + int lfd, ret, line, todel, delt, i, *mexcs, nmexcs, rmexcs; + unsigned char nflags; + struct stat st; + struct flock lck; + char fbuf[16]; /* enlarge when support for keywords is added */ + char buf[64]; + + ret = SYNC_OK; + recs = 0, srecadd = &recs; + + nmmsg = nsmsg = 0; + + mctx->uidvalidity = sctx->uidvalidity = 0; + mopts = sopts = 0; + makeopts( chan->sops, chan->slave, &sopts, chan->master, &mopts ); + makeopts( chan->mops, chan->master, &mopts, chan->slave, &sopts ); + if ((chan->sops & (OP_NEW|OP_RENEW)) && chan->max_messages) + sopts |= OPEN_OLD|OPEN_NEW|OPEN_FLAGS; + if (!mname || (mctx->conf->map_inbox && !strcmp( mctx->conf->map_inbox, mname ))) + mname = "INBOX"; + mctx->name = mname; + mdriver->prepare( mctx, mopts ); + if (!sname || (sctx->conf->map_inbox && !strcmp( sctx->conf->map_inbox, sname ))) + sname = "INBOX"; + sctx->name = sname; + sdriver->prepare( sctx, sopts ); + + if (!strcmp( chan->sync_state ? chan->sync_state : global_sync_state, "*" )) { + if (!sctx->path) { + fprintf( stderr, "Error: store '%s' does not support in-box sync state\n", chan->slave->name ); + return SYNC_SLAVE_BAD; + } + nfasprintf( &dname, "%s/." EXE "state", sctx->path ); + } else { + csname = clean_strdup( sname ); + if (chan->sync_state) + nfasprintf( &dname, "%s%s", chan->sync_state, csname ); + else { + cmname = clean_strdup( mname ); + nfasprintf( &dname, "%s:%s:%s_:%s:%s", global_sync_state, + chan->master->name, cmname, chan->slave->name, csname ); + free( cmname ); + } + free( csname ); + } + nfasprintf( &jname, "%s.journal", dname ); + nfasprintf( &nname, "%s.new", dname ); + nfasprintf( &lname, "%s.lock", dname ); + muidval = suidval = smaxxuid = mmaxuid = smaxuid = 0; + memset( &lck, 0, sizeof(lck) ); +#if SEEK_SET != 0 + lck.l_whence = SEEK_SET; +#endif +#if F_WRLCK != 0 + lck.l_type = F_WRLCK; +#endif + if ((lfd = open( lname, O_WRONLY|O_CREAT, 0666 )) < 0) { + if (errno != ENOENT) { + lferr: + fprintf( stderr, "Error: cannot create lock file %s: %s\n", lname, strerror(errno) ); + ret = SYNC_FAIL; + goto bail2; + } + goto skiprd; + } + if (fcntl( lfd, F_SETLK, &lck )) { + lckerr: + fprintf( stderr, "Error: channel :%s:%s-:%s:%s is locked\n", + chan->master->name, mname, chan->slave->name, sname ); + ret = SYNC_FAIL; + goto bail1; + } + if ((dfp = fopen( dname, "r" ))) { + debug( "reading sync state %s ...\n", dname ); + if (fscanf( dfp, "%d:%d %d:%d:%d\n", &muidval, &mmaxuid, &suidval, &smaxxuid, &smaxuid) != 5) { + fprintf( stderr, "Error: invalid sync state header in %s\n", dname ); + fclose( dfp ); + ret = SYNC_FAIL; + goto bail; + } + line = 1; + while (fgets( buf, sizeof(buf), dfp )) { + line++; + fbuf[0] = 0; + if (sscanf( buf, "%d %d %15s\n", &t1, &t2, fbuf ) < 2) { + fprintf( stderr, "Error: invalid sync state entry at %s:%d\n", dname, line ); + fclose( dfp ); + ret = SYNC_FAIL; + goto bail; + } + srec = nfmalloc( sizeof(*srec) ); + srec->muid = t1; + srec->suid = t2; + s = fbuf; + if (*s == 'X') { + s++; + srec->status = S_EXPIRED; + } else + srec->status = 0; + srec->flags = parse_flags( s ); + debug( " entry (%d,%d,%u,%s)\n", srec->muid, srec->suid, srec->flags, srec->status & S_EXPIRED ? "X" : "" ); + srec->next = 0; + *srecadd = srec; + srecadd = &srec->next; + } + fclose( dfp ); + } else { + if (errno != ENOENT) { + fprintf( stderr, "Error: cannot read sync state %s\n", dname ); + ret = SYNC_FAIL; + goto bail; + } + } + if ((jfp = fopen( jname, "r" ))) { + if (!stat( nname, &st )) { + debug( "recovering journal ...\n" ); + line = 0; + srec = recs; + while (fgets( buf, sizeof(buf), jfp )) { + line++; + if (buf[0] == '^') + srec = recs; + else { + if (buf[0] == '(' || buf[0] == ')' ? + (sscanf( buf + 2, "%d\n", &t1 ) != 1) : + buf[0] == '-' || buf[0] == '|' ? + (sscanf( buf + 2, "%d %d\n", &t1, &t2 ) != 2) : + (sscanf( buf + 2, "%d %d %d\n", &t1, &t2, &t3 ) != 3)) + { + fprintf( stderr, "Error: malformed journal entry at %s:%d\n", jname, line ); + fclose( jfp ); + ret = SYNC_FAIL; + goto bail; + } + if (buf[0] == '(') + mmaxuid = t1; + else if (buf[0] == ')') + smaxuid = t1; + else if (buf[0] == '|') { + muidval = t1; + suidval = t2; + } else if (buf[0] == '+') { + srec = nfmalloc( sizeof(*srec) ); + srec->muid = t1; + srec->suid = t2; + srec->flags = t3; + debug( " new entry(%d,%d,%u)\n", t1, t2, t3 ); + srec->status = 0; + srec->next = 0; + *srecadd = srec; + srecadd = &srec->next; + } else { + for (; srec; srec = srec->next) + if (srec->muid == t1 && srec->suid == t2) + goto syncfnd; + fprintf( stderr, "Error: journal entry at %s:%d refers to non-existing sync state entry\n", jname, line ); + fclose( jfp ); + ret = SYNC_FAIL; + goto bail; + syncfnd: + debug( " entry(%d,%d,%u) ", srec->muid, srec->suid, srec->flags ); + switch (buf[0]) { + case '-': + debug( "killed\n" ); + srec->status = S_DEAD; + break; + case '<': + debug( "master now %d\n", t3 ); + srec->muid = t3; + break; + case '>': + debug( "slave now %d\n", t3 ); + srec->suid = t3; + break; + case '*': + debug( "flags now %d\n", t3 ); + srec->flags = t3; + break; + case '~': + debug( "expired now %d\n", t3 ); + if (t3) { + if (smaxxuid < t2) + smaxxuid = t2; + srec->status |= S_EXPIRED; + } else + srec->status &= ~S_EXPIRED; + break; + default: + fprintf( stderr, "Error: unrecognized journal entry at %s:%d\n", jname, line ); + fclose( jfp ); + ret = SYNC_FAIL; + goto bail; + } + } + } + } + } + fclose( jfp ); + } else { + if (errno != ENOENT) { + fprintf( stderr, "Error: cannot read journal %s\n", jname ); + ret = SYNC_FAIL; + goto bail; + } + } + skiprd: + + if (sctx->opts & OPEN_NEW) + maxwuid = INT_MAX; + else if (sctx->opts & OPEN_OLD) { + maxwuid = 0; + for (srec = recs; srec; srec = srec->next) + if (!(srec->status & S_DEAD) && srec->suid > maxwuid) + maxwuid = srec->suid; + } else + maxwuid = 0; + info( "Selecting slave... " ); + debug( "selecting slave [1,%d]\n", maxwuid ); + switch (sdriver->select( sctx, (sctx->opts & OPEN_OLD) ? 1 : smaxuid + 1, maxwuid, 0, 0 )) { + case DRV_STORE_BAD: ret = SYNC_SLAVE_BAD; goto bail; + case DRV_BOX_BAD: ret = SYNC_FAIL; goto bail; + } + info( "%d messages, %d recent\n", sctx->count, sctx->recent ); + dump_box( sctx ); + + if (suidval && suidval != sctx->uidvalidity) { + fprintf( stderr, "Error: UIDVALIDITY of slave changed\n" ); + ret = SYNC_FAIL; + goto bail; + } + + s = strrchr( dname, '/' ); + *s = 0; + mkdir( dname, 0700 ); + *s = '/'; + if (lfd < 0) { + if ((lfd = open( lname, O_WRONLY|O_CREAT, 0666 )) < 0) + goto lferr; + if (fcntl( lfd, F_SETLK, &lck )) + goto lckerr; } - tmp->processed = 1; - - /* if the message is deleted, and CopyDeletedTo is set, and we - * are expunging, make a copy of the message now. - */ - if (((cur->flags | tmp->flags) & D_DELETED) != 0 && - (flags & SYNC_EXPUNGE) && imap->box->copy_deleted_to) - { - if (imap_copy_message (imap, cur->uid, - imap->box->copy_deleted_to)) - { - fprintf (stderr, - "ERROR: unable to copy deleted message to \"%s\"\n", - imap->box->copy_deleted_to); - return -1; - } + if (!(nfp = fopen( nname, "w" ))) { + fprintf( stderr, "Error: cannot write new sync state %s\n", nname ); + ret = SYNC_FAIL; + goto bail; } + if (!(jfp = fopen( jname, "a" ))) { + fprintf( stderr, "Error: cannot write journal %s\n", jname ); + fclose( nfp ); + ret = SYNC_FAIL; + goto bail; + } + setlinebuf( jfp ); + + mexcs = 0; + nmexcs = rmexcs = 0; + minwuid = INT_MAX; + if (smaxxuid) { + debug( "preparing master selection - max expired slave uid is %d\n", smaxxuid ); + for (srec = recs; srec; srec = srec->next) { + if (srec->status & S_DEAD) + continue; + if (srec->status & S_EXPIRED) { + if (!srec->suid || ((sctx->opts & OPEN_OLD) && !findmsg( sctx, srec->suid, &nsmsg, "slave" ))) + srec->status |= S_EXP_SLAVE; + else if (minwuid > srec->muid) + minwuid = srec->muid; + } else if (smaxxuid < srec->suid && minwuid > srec->muid) + minwuid = srec->muid; + } + debug( " min non-orphaned master uid is %d\n", minwuid ); + fprintf( jfp, "^\n" ); /* if any S_EXP_SLAVE */ + for (srec = recs; srec; srec = srec->next) { + if (srec->status & S_DEAD) + continue; + if (srec->status & S_EXP_SLAVE) { + if (minwuid > srec->muid && mmaxuid >= srec->muid) { + debug( " -> killing (%d,%d)\n", srec->muid, srec->suid ); + srec->status = S_DEAD; + fprintf( jfp, "- %d %d\n", srec->muid, srec->suid ); + } else if (srec->suid) { + debug( " -> orphaning (%d,[%d])\n", srec->muid, srec->suid ); + fprintf( jfp, "> %d %d 0\n", srec->muid, srec->suid ); + srec->suid = 0; + } + } else if (minwuid > srec->muid) { + if (srec->suid < 0) { + if (mmaxuid >= srec->muid) { + debug( " -> killing (%d,%d)\n", srec->muid, srec->suid ); + srec->status = S_DEAD; + fprintf( jfp, "- %d %d\n", srec->muid, srec->suid ); + } + } else if (srec->muid > 0 && srec->suid && (mctx->opts & OPEN_OLD) && + (!(mctx->opts & OPEN_NEW) || mmaxuid >= srec->muid)) { + if (nmexcs == rmexcs) { + rmexcs = rmexcs * 2 + 100; + mexcs = nfrealloc( mexcs, rmexcs * sizeof(int) ); + } + mexcs[nmexcs++] = srec->muid; + } + } + } + debug( " exception list is:" ); + for (i = 0; i < nmexcs; i++) + debug( " %d", mexcs[i] ); + debug( "\n" ); + } else if (mctx->opts & OPEN_OLD) + minwuid = 1; + if (mctx->opts & OPEN_NEW) { + if (minwuid > mmaxuid + 1) + minwuid = mmaxuid + 1; + maxwuid = INT_MAX; + } else if (mctx->opts & OPEN_OLD) { + maxwuid = 0; + for (srec = recs; srec; srec = srec->next) + if (!(srec->status & S_DEAD) && srec->muid > maxwuid) + maxwuid = srec->muid; + } else + maxwuid = 0; + info( "Selecting master... " ); + debug( "selecting master [%d,%d]\n", minwuid, maxwuid ); + switch (mdriver->select( mctx, minwuid, maxwuid, mexcs, nmexcs )) { + case DRV_STORE_BAD: ret = SYNC_MASTER_BAD; goto finish; + case DRV_BOX_BAD: ret = SYNC_FAIL; goto finish; + } + info( "%d messages, %d recent\n", mctx->count, mctx->recent ); + dump_box( mctx ); - /* 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 */ - if (!(tmp->flags & D_DELETED) && (cur->flags & D_DELETED)) - imap->deleted++; - - imap_set_flags (imap, cur->uid, cur->flags & ~tmp->flags); - - /* update local flags */ - if ((cur->flags & D_DELETED) == 0 && (tmp->flags & D_DELETED)) - mbox->deleted++; - cur->flags |= (tmp->flags & ~(D_RECENT | D_DRAFT)); - - /* don't bother renaming the file if we are just going to - * remove it later. - */ - if ((cur->flags & D_DELETED) == 0 || (flags & SYNC_EXPUNGE) == 0) - { - size_t sl; - - /* generate old path */ - snprintf (path, sizeof (path), "%s/%s/%s", - mbox->path, cur->new ? "new" : "cur", cur->file); - - /* generate new path */ - strcpy (newpath, path); - p = strchr (newpath, ':'); - sl = p ? (size_t)(p - newpath) : strlen (newpath); - snprintf (newpath + sl, sizeof (newpath) - sl, ":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" : ""); - - if (rename (path, newpath)) - { - perror ("Warning: cannot set flags on message"); - } - else - { - /* update the filename in the msg struct */ - p = strrchr (newpath, '/'); - free (cur->file); - cur->file = strdup (p + 1); - } - } + if (muidval && muidval != mctx->uidvalidity) { + fprintf( stderr, "Error: UIDVALIDITY of master changed\n" ); + ret = SYNC_FAIL; + goto finish; } - } - - if (upload) - info (" %d messages.\n", upload); - - if (max_msgs == 0) - max_msgs = UINT_MAX; - else - { - /* expire messages in excess of the max-count for this mailbox. - * flagged mails are considered sacrosant and not deleted. - * we have already done the upload to the server, so messing with - * the flags variable do not have remote side effects. - */ - for (cur = imap->msgs, msg_count = 0; - cur && msg_count < max_msgs; cur = cur->next, msg_count++) - { - tmp = find_msg (mbox->msgs, cur->uid); - if (tmp) - tmp->wanted = 1; + + if (!muidval || !suidval) { + muidval = mctx->uidvalidity; + suidval = sctx->uidvalidity; + fprintf( jfp, "| %d %d\n", muidval, suidval ); } - for (cur = mbox->msgs; cur; cur = cur->next) - { - if (!cur->wanted && !(cur->flags & D_FLAGGED)) - { - cur->flags |= D_DELETED; - cur->dead = 1; - mbox->deleted++; - } + + info( "Synchronizing\n" ); + debug( "synchronizing old entries\n" ); + fprintf( jfp, "^\n" ); + for (srec = recs; srec; srec = srec->next) { + if (srec->status & S_DEAD) + continue; + debug( "pair (%d,%d)\n", srec->muid, srec->suid ); + mmsg = findmsg( mctx, srec->muid, &nmmsg, "master" ); + smsg = (srec->status & S_EXP_SLAVE) ? 0 : findmsg( sctx, srec->suid, &nsmsg, "slave" ); + nom = !mmsg && (mctx->opts & OPEN_OLD); + nos = !smsg && (sctx->opts & OPEN_OLD); + if (nom && nos) { + debug( " vanished\n" ); + /* d.1) d.5) d.6) d.10) d.11) */ + srec->status = S_DEAD; + fprintf( jfp, "- %d %d\n", srec->muid, srec->suid ); + } else { + delm = nom && (srec->muid > 0); + dels = nos && (srec->suid > 0); + nflags = srec->flags; + + if ((ret = sync_old( chan->mops, sctx, mctx, chan->master, jfp, 0, &nflags, srec, smsg, mmsg, dels, delm )) != SYNC_OK || + (ret = sync_old( chan->sops, mctx, sctx, chan->slave, jfp, 1, &nflags, srec, mmsg, smsg, delm, dels )) != SYNC_OK) + goto finish; + + if (srec->flags != nflags) { + debug( " updating flags (%u -> %u)\n", srec->flags, nflags ); + srec->flags = nflags; + fprintf( jfp, "* %d %d %u\n", srec->muid, srec->suid, nflags ); + } + if (mmsg && (mmsg->flags & F_DELETED)) + srec->status |= S_DEL_MASTER; + if (smsg && (smsg->flags & F_DELETED)) + srec->status |= S_DEL_SLAVE; + } } - } - - for (cur = imap->msgs, msg_count = 0; - cur && msg_count < max_msgs; cur = cur->next, msg_count++) - { - if (!cur->processed) - { - /* new message on server */ - - if ((flags & SYNC_EXPUNGE) && (cur->flags & D_DELETED)) - { - /* this message has been marked for deletion and - * we are currently expunging a mailbox. don't - * bother downloading this message - */ - continue; - } - - if (max_size && cur->size > max_size) - { - info ("Remote message %u skipped because it is too big (%u)\n", - cur->uid, cur->size); - continue; - } - - /* construct the flags part of the file name. */ - - *suffix = 0; - if (cur->flags & ~(D_RECENT | D_DRAFT)) - { - snprintf (suffix, sizeof (suffix), ":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" : ""); - } - - for (;;) - { - /* create new file */ - snprintf (path, sizeof (path), "%s/tmp/%ld_%d.%d.%s%s", - mbox->path, time (0), MaildirCount++, getpid (), - Hostname, suffix); - - if ((fd = open (path, O_WRONLY | O_CREAT | O_EXCL, 0600)) > 0) - break; - if (errno != EEXIST) - { - perror (path); - break; + + debug( "synchronizing new entries\n" ); + if ((ret = sync_new( chan->mops, sctx, mctx, chan->master, jfp, &srecadd, 0, &smaxuid )) != SYNC_OK || + (ret = sync_new( chan->sops, mctx, sctx, chan->slave, jfp, &srecadd, 1, &mmaxuid )) != SYNC_OK) + goto finish; + + if ((chan->sops & (OP_NEW|OP_RENEW)) && chan->max_messages) { + debug( "expiring excessive entries\n" ); + todel = sctx->count - chan->max_messages; + for (smsg = sctx->msgs; smsg && todel > 0; smsg = smsg->next) + if (!(smsg->status & M_DEAD) && (smsg->flags & F_DELETED)) + todel--; + delt = 0; + for (smsg = sctx->msgs; smsg && todel > 0; smsg = smsg->next) { + if ((smsg->status & M_DEAD) || (smsg->flags & F_DELETED)) + continue; + if ((smsg->flags & F_FLAGGED) || (smsg->status & M_NOT_SYNCED)) /* add M_DESYNCED? */ + todel--; + else if (!(smsg->status & M_RECENT)) { + smsg->status |= M_EXPIRED; + delt++; + todel--; + } + } + if (delt) { + fprintf( jfp, "^\n" ); + for (srec = recs; srec; srec = srec->next) { + if (srec->status & (S_DEAD|S_EXPIRED)) + continue; + smsg = findmsg( sctx, srec->suid, &nsmsg, "slave" ); + if (smsg && (smsg->status & M_EXPIRED)) { + debug( " expiring pair(%d,%d)\n", srec->muid, srec->suid ); + /* log first, so deletion can't be misinterpreted! */ + fprintf( jfp, "~ %d %d 1\n", srec->muid, srec->suid ); + if (smaxxuid < srec->suid) + smaxxuid = srec->suid; + srec->status |= S_EXPIRED; + switch (sdriver->set_flags( sctx, smsg, 0, F_DELETED, 0 )) { + case DRV_STORE_BAD: ret = SYNC_SLAVE_BAD; goto finish; + case DRV_BOX_BAD: ret = SYNC_FAIL; goto finish; + default: /* ok */ break; + case DRV_OK: srec->status |= S_DEL_SLAVE; + } + } + } } + } - sleep (2); - } - - if (fd < 0) - continue; - - /* give some visual feedback that something is happening */ - if (!fetched) - info ("Fetching new messages"); - infoc ('.'); - fflush (stdout); - fetched++; - - ret = imap_fetch_message (imap, cur->uid, fd); - - if (fsync (fd)) - { - perror ("fsync"); - close (fd); - } - else if (close (fd)) - perror ("close"); - else if (!ret) - { - p = strrchr (path, '/'); - - snprintf (newpath, sizeof (newpath), "%s/%s%s", mbox->path, - (cur->flags & D_SEEN) ? "cur" : "new", p); /* hack: ignore \recent, use !\seen instead */ - - /* its ok if this fails, the next time we sync the message - * will get pulled down - */ - if (link (path, newpath)) - perror ("link"); - else - { - /* update the db with the UID mapping for this file */ - set_uid (mbox->db, p + 1, cur->uid); - if (cur->uid > mbox->maxuid) - mbox->maxuid = cur->uid; + /* Doing CLOSE here instead of EXPUNGE above saves network traffic. + But it costs more server power for single-file formats. And it + makes disk-full/quota-exceeded more probable. */ + mex = sex = 0; + if (chan->mops & OP_EXPUNGE) { + info( "Expunging master\n" ); + debug( "expunging master\n" ); + switch (expunge( mctx, sctx )) { + case EX_STORE_BAD: ret = SYNC_MASTER_BAD; goto finish; + case EX_RSTORE_BAD: ret = SYNC_SLAVE_BAD; goto finish; + default: ret = SYNC_FAIL; break; + case EX_OK: mex = 1; + } + } + if (chan->sops & OP_EXPUNGE) { + info( "Expunging slave\n" ); + debug( "expunging slave\n" ); + switch (expunge( sctx, mctx )) { + case EX_STORE_BAD: ret = SYNC_SLAVE_BAD; goto finish; + case EX_RSTORE_BAD: ret = SYNC_MASTER_BAD; goto finish; + default: ret = SYNC_FAIL; break; + case EX_OK: mex = 1; + } + } + if (mex || sex) { + /* This cleanup is not strictly necessary, as the next full sync + would throw out the dead entries anyway. But ... */ + + minwuid = INT_MAX; + if (smaxxuid) { + debug( "preparing entry purge - max expired slave uid is %d\n", smaxxuid ); + for (srec = recs; srec; srec = srec->next) { + if (srec->status & S_DEAD) + continue; + if (!((srec->suid <= 0 || ((srec->status & S_DEL_SLAVE) && sex)) && + (srec->muid <= 0 || ((srec->status & S_DEL_MASTER) && mex) || (srec->status & S_EXPIRED))) && + smaxxuid < srec->suid && minwuid > srec->muid) + minwuid = srec->muid; + } + debug( " min non-orphaned master uid is %d\n", minwuid ); } - } - /* always remove the temp file */ - unlink (path); + fprintf( jfp, "^\n" ); + for (srec = recs; srec; srec = srec->next) { + if (srec->status & S_DEAD) + continue; + if (srec->suid <= 0 || ((srec->status & S_DEL_SLAVE) && sex)) { + if (srec->muid <= 0 || ((srec->status & S_DEL_MASTER) && mex)) { + debug( " -> killing (%d,%d)\n", srec->muid, srec->suid ); + srec->status = S_DEAD; + fprintf( jfp, "- %d %d\n", srec->muid, srec->suid ); + } else if (srec->status & S_EXPIRED) { + if (mmaxuid >= srec->muid && minwuid > srec->muid) { + debug( " -> killing (%d,%d)\n", srec->muid, srec->suid ); + srec->status = S_DEAD; + fprintf( jfp, "- %d %d\n", srec->muid, srec->suid ); + } else if (srec->suid) { + debug( " -> orphaning (%d,[%d])\n", srec->muid, srec->suid ); + fprintf( jfp, "> %d %d 0\n", srec->muid, srec->suid ); + srec->suid = 0; + } + } + } + } } - } - if (fetched) - info (" %d messages\n", fetched); + finish: + fprintf( nfp, "%d:%d %d:%d:%d\n", muidval, mmaxuid, suidval, smaxxuid, smaxuid ); + for (srec = recs; srec; srec = srec->next) { + if (srec->status & S_DEAD) + continue; + make_flags( srec->flags, fbuf ); + fprintf( nfp, "%d %d %s%s\n", srec->muid, srec->suid, + srec->status & S_EXPIRED ? "X" : "", fbuf ); + } - if (maildir_update_maxuid (mbox)) - return -1; + fclose( nfp ); + fclose( jfp ); + /* order is important! */ + rename( nname, dname ); + unlink( jname ); - return 0; + bail: + for (srec = recs; srec; srec = nsrec) { + nsrec = srec->next; + free( srec ); + } + unlink( lname ); + bail1: + close( lfd ); + bail2: + free( lname ); + free( nname ); + free( jname ); + free( dname ); + return ret; } + diff --git a/src/util.c b/src/util.c @@ -0,0 +1,389 @@ +/* + * mbsync - mailbox synchronizer + * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> + * Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net> + * + * 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 + * + * As a special exception, mbsync may be linked with the OpenSSL library, + * despite that library's more restrictive license. + */ + +#include "isync.h" + +#include <stdarg.h> +#include <stdlib.h> +#include <unistd.h> +#include <fcntl.h> +#include <string.h> +#include <pwd.h> +#include <ctype.h> + +int Verbose, Quiet, Debug; + +void +debug( const char *msg, ... ) +{ + va_list va; + + if (Debug) { + va_start( va, msg ); + vprintf( msg, va ); + va_end( va ); + } +} + +void +info( const char *msg, ... ) +{ + va_list va; + + if (!Quiet) { + va_start( va, msg ); + vprintf( msg, va ); + va_end( va ); + fflush( stdout ); + } +} + +void +infoc( char c ) +{ + if (!Quiet) { + putchar( c ); + fflush( stdout ); + } +} + +void +warn( const char *msg, ... ) +{ + va_list va; + + if (Quiet < 2) { + va_start( va, msg ); + vfprintf( stderr, msg, va ); + va_end( va ); + } +} + +char * +next_arg( char **s ) +{ + char *ret; + + if (!s || !*s) + return 0; + while (isspace( (unsigned char) **s )) + (*s)++; + if (!**s) { + *s = 0; + return 0; + } + if (**s == '"') { + ++*s; + ret = *s; + *s = strchr( *s, '"' ); + } else { + ret = *s; + while (**s && !isspace( (unsigned char) **s )) + (*s)++; + } + if (*s) { + if (**s) + *(*s)++ = 0; + if (!**s) + *s = 0; + } + return ret; +} + +void +add_string_list( string_list_t **list, const char *str ) +{ + string_list_t *elem; + int len; + + len = strlen( str ); + elem = nfmalloc( sizeof(*elem) + len ); + elem->next = *list; + *list = elem; + memcpy( elem->string, str, len + 1 ); +} + +void +free_string_list( string_list_t *list ) +{ + string_list_t *tlist; + + for (; list; list = tlist) { + tlist = list->next; + free( list ); + } +} + +void +free_generic_messages( message_t *msgs ) +{ + message_t *tmsg; + + for (; msgs; msgs = tmsg) { + tmsg = msgs->next; + free( msgs ); + } +} + +void +strip_cr( msg_data_t *msgdata ) +{ + int i, o; + + if (msgdata->crlf) { + for (i = o = 0; i < msgdata->len; i++) + if (msgdata->data[i] != '\r') + msgdata->data[o++] = msgdata->data[i]; + msgdata->len = o; + msgdata->crlf = 0; + } +} + +#ifndef HAVE_VASPRINTF +static int +vasprintf( char **strp, const char *fmt, va_list ap ) +{ + int len; + char tmp[1024]; + + if ((len = vsnprintf( tmp, sizeof(tmp), fmt, ap) ) < 0) + *strp = 0; + else if ((*strp = malloc( len + 1 ))) { + if (len >= sizeof(tmp)) + vsprintf( *strp, fmt, ap ); + else + memcpy( *strp, tmp, len + 1 ); + } + return len; +} +#endif + +void +oob( void ) +{ + fputs( "Fatal: buffer too small. Please report a bug.\n", stderr ); + abort(); +} + +int +nfsnprintf( char *buf, int blen, const char *fmt, ... ) +{ + int ret; + va_list va; + + va_start( va, fmt ); + if (blen <= 0 || (unsigned)(ret = vsnprintf( buf, blen, fmt, va )) >= (unsigned)blen) + oob(); + va_end( va ); + return ret; +} + +static void ATTR_NORETURN +oom( void ) +{ + fputs( "Fatal: Out of memory\n", stderr ); + abort(); +} + +void * +nfmalloc( size_t sz ) +{ + void *ret; + + if (!(ret = malloc( sz ))) + oom(); + return ret; +} + +void * +nfcalloc( size_t sz ) +{ + void *ret; + + if (!(ret = calloc( sz, 1 ))) + oom(); + return ret; +} + +void * +nfrealloc( void *mem, size_t sz ) +{ + char *ret; + + if (!(ret = realloc( mem, sz )) && sz) + oom(); + return ret; +} + +char * +nfstrdup( const char *str ) +{ + char *ret; + + if (!(ret = strdup( str ))) + oom(); + return ret; +} + +int +nfvasprintf( char **str, const char *fmt, va_list va ) +{ + int ret = vasprintf( str, fmt, va ); + if (!*str) + oom(); + return ret; +} + +int +nfasprintf( char **str, const char *fmt, ... ) +{ + int ret; + va_list va; + + va_start( va, fmt ); + ret = nfvasprintf( str, fmt, va ); + va_end( va ); + return ret; +} + +/* +static struct passwd * +cur_user( void ) +{ + char *p; + struct passwd *pw; + uid_t uid; + + uid = getuid(); + if ((!(p = getenv("LOGNAME")) || !(pw = getpwnam( p )) || pw->pw_uid != uid) && + (!(p = getenv("USER")) || !(pw = getpwnam( p )) || pw->pw_uid != uid) && + !(pw = getpwuid( uid ))) + { + fputs ("Cannot determinate current user\n", stderr); + return 0; + } + return pw; +} +*/ + +static char * +my_strndup( const char *s, size_t nchars ) +{ + char *r = nfmalloc( nchars + 1 ); + memcpy( r, s, nchars ); + r[nchars] = 0; + return r; +} + +char * +expand_strdup( const char *s ) +{ + struct passwd *pw; + const char *p, *q; + char *r; + + if (*s == '~') { + s++; + if (!*s) { + p = 0; + q = Home; + } else if (*s == '/') { + p = s; + q = Home; + } else { + if ((p = strchr( s, '/' ))) { + r = my_strndup( s, (int)(p - s) ); + pw = getpwnam( r ); + free( r ); + } else + pw = getpwnam( s ); + if (!pw) + return 0; + q = pw->pw_dir; + } + nfasprintf( &r, "%s%s", q, p ? p : "" ); + return r; + } else + return nfstrdup( s ); +} + +static int +compare_ints( const void *l, const void *r ) +{ + return *(int *)l - *(int *)r; +} + +void +sort_ints( int *arr, int len ) +{ + qsort( arr, len, sizeof(int), compare_ints ); +} + + +static struct { + unsigned char i, j, s[256]; +} rs; + +void +arc4_init( void ) +{ + int i, fd; + unsigned char j, si, dat[128]; + + if ((fd = open( "/dev/urandom", O_RDONLY )) < 0 && (fd = open( "/dev/random", O_RDONLY )) < 0) { + fprintf( stderr, "Fatal: no random number source available.\n" ); + exit( 3 ); + } + if (read( fd, dat, 128 ) != 128) { + fprintf( stderr, "Fatal: cannot read random number source.\n" ); + exit( 3 ); + } + close( fd ); + + for (i = 0; i < 256; i++) + rs.s[i] = i; + for (i = j = 0; i < 256; i++) { + si = rs.s[i]; + j += si + dat[i & 127]; + rs.s[i] = rs.s[j]; + rs.s[j] = si; + } + rs.i = rs.j = 0; + + for (i = 0; i < 256; i++) + arc4_getbyte(); +} + +unsigned char +arc4_getbyte( void ) +{ + unsigned char si, sj; + + rs.i++; + si = rs.s[rs.i]; + rs.j += si; + sj = rs.s[rs.j]; + rs.s[rs.i] = sj; + rs.s[rs.j] = si; + return rs.s[(si + sj) & 0xff]; +}