diff --git a/offlineimap.conf b/offlineimap.conf index 9263df7..43cabaf 100644 --- a/offlineimap.conf +++ b/offlineimap.conf @@ -203,7 +203,7 @@ restoreatime = no [Repository RemoteExample] -# And this is the remote repository. For now, we only support IMAP here. +# And this is the remote repository. We only support IMAP or Gmail here. type = IMAP @@ -380,3 +380,33 @@ holdconnectionopen = no # # foldersort = lambda x, y: -cmp(x, y) + +[Repository GmailExample] + +# A repository using Gmail's IMAP interface. Any configuration +# parameter of `IMAP` type repositories can be used here. Only +# `remoteuser` (or `remoteusereval` ) is mandatory. Default values +# for other parameters are OK, and you should not need fiddle with +# those. +# +# The Gmail repository will use hard-coded values for `remotehost`, +# `remoteport`, `tunnel` and `ssl`. (See +# http://mail.google.com/support/bin/answer.py?answer=78799&topic=12814) +# Any attempt to set those parameters will be silently ignored. +# + +type = Gmail + +# Specify the Gmail user name. This is the only mandatory parameter. +remoteuser = username@gmail.com + +# Deleting a message from a Gmail folder via the IMAP interface will +# just remove that folder's label from the message: the message will +# continue to exist in the '[Gmail]/All Mail' folder. If `realdelete` +# is set to `True`, then deleted messages will really be deleted +# during `offlineimap` sync, by moving them to the '[Gmail]/Trash' +# folder. BEWARE: this will deleted a messages from *all folders* it +# belongs to! +# +# See http://mail.google.com/support/bin/answer.py?answer=77657&topic=12815 +realdelete = no diff --git a/offlineimap.sgml b/offlineimap.sgml index c0498d1..75ad297 100644 --- a/offlineimap.sgml +++ b/offlineimap.sgml @@ -32,6 +32,8 @@ -a accountlist -c configfile -d debugtype[,...] + -f foldername[,...] + -k [section:]option=value -l filename -o -u interface @@ -204,6 +206,8 @@ remoteuser = jgoerzen and corporate networks do, and most operating systems have an IMAP implementation readily available. + A special Gmail mailbox type is + available to interface with Gmail's IMAP front-end. diff --git a/offlineimap/folder/Gmail.py b/offlineimap/folder/Gmail.py new file mode 100644 index 0000000..bf040e9 --- /dev/null +++ b/offlineimap/folder/Gmail.py @@ -0,0 +1,119 @@ +# Gmail IMAP folder support +# Copyright (C) 2008 Riccardo Murri +# Copyright (C) 2002-2007 John Goerzen +# +# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""Folder implementation to support features of the Gmail IMAP server. +""" + +from IMAP import IMAPFolder +import imaplib +from offlineimap import imaputil, imaplibutil +from offlineimap.ui import UIBase +from copy import copy + + +class GmailFolder(IMAPFolder): + """Folder implementation to support features of the Gmail IMAP server. + Specifically, deleted messages are moved to folder `Gmail.TRASH_FOLDER` + (by default: ``[Gmail]/Trash``) prior to expunging them, since + Gmail maps to IMAP ``EXPUNGE`` command to "remove label". + + For more information on the Gmail IMAP server: + http://mail.google.com/support/bin/answer.py?answer=77657&topic=12815 + """ + + #: Where deleted mail should be moved + TRASH_FOLDER='[Gmail]/Trash' + + #: Gmail will really delete messages upon EXPUNGE in these folders + REAL_DELETE_FOLDERS = [ TRASH_FOLDER, '[Gmail]/Spam' ] + + def __init__(self, imapserver, name, visiblename, accountname, repository): + self.realdelete = repository.getrealdelete(name) + IMAPFolder.__init__(self, imapserver, name, visiblename, \ + accountname, repository) + + def deletemessages_noconvert(self, uidlist): + uidlist = [uid for uid in uidlist if uid in self.messagelist] + if not len(uidlist): + return + + if self.realdelete and not (self.getname() in self.REAL_DELETE_FOLDERS): + # IMAP expunge is just "remove label" in this folder, + # so map the request into a "move into Trash" + + imapobj = self.imapserver.acquireconnection() + try: + imapobj.select(self.getfullname()) + result = imapobj.uid('copy', + imaputil.listjoin(uidlist), + self.TRASH_FOLDER) + assert result[0] == 'OK', \ + "Bad IMAPlib result: %s" % result[0] + finally: + self.imapserver.releaseconnection(imapobj) + for uid in uidlist: + del self.messagelist[uid] + else: + IMAPFolder.deletemessages_noconvert(self, uidlist) + + def processmessagesflags(self, operation, uidlist, flags): + # XXX: the imapobj.myrights(...) calls dies with an error + # report from Gmail server stating that IMAP command + # 'MYRIGHTS' is not implemented. So, this + # `processmessagesflags` is just a copy from `IMAPFolder`, + # with the references to `imapobj.myrights()` deleted This + # shouldn't hurt, however, Gmail users always have full + # control over all their mailboxes (apparently). + if len(uidlist) > 101: + # Hack for those IMAP ervers with a limited line length + self.processmessagesflags(operation, uidlist[:100], flags) + self.processmessagesflags(operation, uidlist[100:], flags) + return + + imapobj = self.imapserver.acquireconnection() + try: + imapobj.select(self.getfullname()) + r = imapobj.uid('store', + imaputil.listjoin(uidlist), + operation + 'FLAGS', + imaputil.flagsmaildir2imap(flags)) + assert r[0] == 'OK', 'Error with store: ' + '. '.join(r[1]) + r = r[1] + finally: + self.imapserver.releaseconnection(imapobj) + + needupdate = copy(uidlist) + for result in r: + attributehash = imaputil.flags2hash(imaputil.imapsplit(result)[1]) + flags = attributehash['FLAGS'] + uid = long(attributehash['UID']) + self.messagelist[uid]['flags'] = imaputil.flagsimap2maildir(flags) + try: + needupdate.remove(uid) + except ValueError: # Let it slide if it's not in the list + pass + for uid in needupdate: + if operation == '+': + for flag in flags: + if not flag in self.messagelist[uid]['flags']: + self.messagelist[uid]['flags'].append(flag) + self.messagelist[uid]['flags'].sort() + elif operation == '-': + for flag in flags: + if flag in self.messagelist[uid]['flags']: + self.messagelist[uid]['flags'].remove(flag) diff --git a/offlineimap/folder/__init__.py b/offlineimap/folder/__init__.py index bcdb844..425148b 100644 --- a/offlineimap/folder/__init__.py +++ b/offlineimap/folder/__init__.py @@ -1,2 +1,2 @@ -import Base, IMAP, Maildir, LocalStatus +import Base, Gmail, IMAP, Maildir, LocalStatus diff --git a/offlineimap/repository/Base.py b/offlineimap/repository/Base.py index a6c62cd..93e464b 100644 --- a/offlineimap/repository/Base.py +++ b/offlineimap/repository/Base.py @@ -20,11 +20,13 @@ from offlineimap import CustomConfig import os.path def LoadRepository(name, account, reqtype): + from offlineimap.repository.Gmail import GmailRepository from offlineimap.repository.IMAP import IMAPRepository, MappedIMAPRepository from offlineimap.repository.Maildir import MaildirRepository if reqtype == 'remote': # For now, we don't support Maildirs on the remote side. - typemap = {'IMAP': IMAPRepository} + typemap = {'IMAP': IMAPRepository, + 'Gmail': GmailRepository} elif reqtype == 'local': typemap = {'IMAP': MappedIMAPRepository, 'Maildir': MaildirRepository} diff --git a/offlineimap/repository/Gmail.py b/offlineimap/repository/Gmail.py new file mode 100644 index 0000000..f26e7b9 --- /dev/null +++ b/offlineimap/repository/Gmail.py @@ -0,0 +1,68 @@ +# Gmail IMAP repository support +# Copyright (C) 2008 Riccardo Murri +# +# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from IMAP import IMAPRepository +from offlineimap import folder, imaputil +from offlineimap.imapserver import IMAPServer + +class GmailRepository(IMAPRepository): + """Gmail IMAP repository. + + Uses hard-coded host name and port, see: + http://mail.google.com/support/bin/answer.py?answer=78799&topic=12814 + """ + + #: Gmail IMAP server hostname + HOSTNAME = "imap.gmail.com" + + #: Gmail IMAP server port + PORT = 993 + + def __init__(self, reposname, account): + """Initialize a GmailRepository object.""" + account.getconfig().set('Repository ' + reposname, + 'remotehost', GmailRepository.HOSTNAME) + account.getconfig().set('Repository ' + reposname, + 'remoteport', GmailRepository.PORT) + account.getconfig().set('Repository ' + reposname, + 'ssl', 'yes') + IMAPRepository.__init__(self, reposname, account) + + def gethost(self): + return GmailRepository.HOSTNAME + + def getport(self): + return GmailRepository.PORT + + def getssl(self): + return 1 + + def getpreauthtunnel(self): + return None + + def getfolder(self, foldername): + return self.getfoldertype()(self.imapserver, foldername, + self.nametrans(foldername), + self.accountname, self) + + def getfoldertype(self): + return folder.Gmail.GmailFolder + + def getrealdelete(self, foldername): + # XXX: `foldername` is currently ignored - the `realdelete` + # setting is repository-wide + return self.getconfboolean('realdelete', 0) diff --git a/offlineimap/repository/__init__.py b/offlineimap/repository/__init__.py index feac780..be5c29e 100644 --- a/offlineimap/repository/__init__.py +++ b/offlineimap/repository/__init__.py @@ -1 +1 @@ -__all__ = ['IMAP', 'Base', 'Maildir', 'LocalStatus'] +__all__ = ['Gmail', 'IMAP', 'Base', 'Maildir', 'LocalStatus']