So here's the script I'm currently using to synchronize a maildir. #!/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)))) if mvlist: mvcommands = string.join(mvlist, '\; ') rv = os.system("ssh -C %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 -C %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 -avzz %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() # - didn't handle the case where no messages are renamed # - didn't use compression (oops!) -- <[EMAIL PROTECTED]> Kragen Sitaker <http://www.pobox.com/~kragen/> Irony and sarcasm deflate seriousness, and when your seriousness becomes detum- escent, you're not held responsible for your thoughts. Irony beats thinking like rock beats scissors. -- http://www.hyperorg.com/backissues/joho-june2-98.html