#!/usr/bin/env python
"""Synchronize a maildir with rsync.

I have some maildirs on a remote machine.  The MTA on the remote
machine delivers into my maildirs there, but then I download the
contents of the maildirs using rsync to my local machine, where I read
them.

The trouble is that I can't mark those messages as "read" on the local
machine by moving them from new/ to cur/, because the next time I run
rsync to download the messages, they will be copied back into new/.
So a simple solution is to move those messages on the remote machine
before running rsync.

That's what this program does; it seems to work at least for one
particular mailbox on one particular remote host, for me.  It will be
painful to use if your ssh is set up to require you to type a password
or passphrase for every connection, because it makes four different
ssh connections.

I should probably switch to something sane like IMAP or POP, no?  Or
maybe put my mailboxes in newsgroups...

BUGS:
- it doesn't work when local and remote directories are not the same
  (yeah, I know, that's a really dumb bug)
- doesn't use ssh compression, not even for the file lists
- message *deletion* still doesn't get propagated upstream, because there's
  no way to tell the difference between a message that has been received on
  this machine and then deleted and a message that has not yet been received
  on this machine.  Hmm, well, maybe if it was in cur/ on the remote
  machine, we could assume it had been read...
- status changes produced on the remote host (say, by reading it with
  a MUA there) are not propagated downstream, and are in fact undone

"""

import os, sys, operator, string

def locations(filelist):
    rv = {}
    for file in filelist:
        # not found means use beginning of string
        lastslash = string.rfind(file, '/') + 1
        lastcolon = string.rfind(file, ':')
        if lastcolon == -1: lastcolon = len(file)
        rv[file[lastslash:lastcolon]] = file
    return rv

def renamelocations(renamefunc, current_state, desired_state):
    current_locations = locations(current_state)
    desired_locations = locations(desired_state)
    curdeslist = []
    for message in desired_locations.keys():
        if current_locations.has_key(message):
            cur = current_locations[message]
            des = desired_locations[message]
            if cur != des:
                curdeslist.append((cur, des))
    renamefunc(curdeslist)

def shellquote(astring):
    "Given a string, return a quoted string the shell will evaluate to it."
    return "'%s'" % string.replace(string.replace(astring, '\\', '\\\\'),
                                   "'", "'\\''")

class rename_with_ssh:
    def __init__(self, remotehost):
        self.remotehost = remotehost
    def __call__(self, curdeslist):
        mvlist = []
        for cur, des in curdeslist:
            print "renaming %s to %s on %s" % (cur, des, self.remotehost)
            mvlist.append("mv %s %s" % (shellquote(shellquote(cur)),
                                        shellquote(shellquote(des))))
        mvcommands = string.join(mvlist, '\; ')
        rv = os.system("ssh %s %s" % (shellquote(self.remotehost),
                                      mvcommands))
        if rv != 0:
            raise RuntimeError, "mv failed with %s" % rv

def chomp(astring):
    if astring[-1:] == '\n': return astring[:-1]
    return astring

def get_file_list(cmd):
    lister = os.popen(cmd)
    rv = map(chomp, lister.readlines())
    failcode = lister.close()
    if failcode is not None:
        raise RuntimeError, "cmd failed with %s: %s" % (failcode, cmd)
    return rv

def get_local_file_list(dirname):
    return get_file_list("find %s -type f" % shellquote(dirname))

def get_remote_file_list(hostname, dirname):
    return get_file_list("ssh %s find %s -type f" % (shellquote(hostname),
                                                     shellquote(dirname)))

def syncremotehost(hostname, remotedir, localdir):
    localfiles = get_local_file_list(localdir)
    remotefiles = get_remote_file_list(hostname, remotedir)
    renamelocations(rename_with_ssh(hostname), remotefiles, localfiles)

def syncmail(hostname, remotedir, localdir):
    syncremotehost(hostname, remotedir, localdir)
    rv = os.system("rsync -e ssh -av %s:%s/ %s" % (shellquote(hostname),
                                                   shellquote(remotedir),
                                                   shellquote(localdir)))
    if rv != 0:
        raise RuntimeError, "rsync failed with %s" % rv

if __name__ == '__main__':
    if (len(sys.argv) == 4 and
        sys.argv[2][-1] != '/' and
        sys.argv[3][-1] != '/'):
        syncmail(sys.argv[1], sys.argv[2], sys.argv[3])
        sys.exit(0)
    else:
        sys.stderr.write("Usage: %s remotehost remotemaildir localmaildir\n" %
                         sys.argv[0])
        sys.exit(1)

# fixed bug log:
# - forgot \n on usage message
# - wrote 'else return astring' without a ':' after 'else'
# - forgot to return rv from locations()
# - the following incompatibilities with 1.5.2:
#   - used string methods .replace and .rfind
#   - used zip()

-- 
<[EMAIL PROTECTED]>       Kragen Sitaker     <http://www.pobox.com/~kragen/>
Operating personal computers now requires us to devote as much time to
set-up menus, installation programs, configuration "wizards", and help
databases as we do running productive applications.  --- Carl Sassenrath


Reply via email to