Add Gmail IMAP special support.

New repository/folder classes to support "real deletion" of messages
thorugh Gmail's IMAP interface: to really delete a message in Gmail,
one has to move it to the Trash folder, rather than EXPUNGE it.
This commit is contained in:
Riccardo Murri 2008-01-03 04:56:55 +01:00
parent ec89c3eb53
commit 81b86fb74c
7 changed files with 227 additions and 4 deletions

View File

@ -203,7 +203,7 @@ restoreatime = no
[Repository RemoteExample] [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 type = IMAP
@ -380,3 +380,33 @@ holdconnectionopen = no
# #
# foldersort = lambda x, y: -cmp(x, y) # 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

View File

@ -32,6 +32,8 @@
<arg>-a <replaceable>accountlist</replaceable></arg> <arg>-a <replaceable>accountlist</replaceable></arg>
<arg>-c <replaceable>configfile</replaceable></arg> <arg>-c <replaceable>configfile</replaceable></arg>
<arg>-d <replaceable>debugtype[,...]</replaceable></arg> <arg>-d <replaceable>debugtype[,...]</replaceable></arg>
<arg>-f <replaceable>foldername[,...]</replaceable></arg>
<arg>-k <replaceable>[section:]option=value</replaceable></arg>
<arg>-l <replaceable>filename</replaceable></arg> <arg>-l <replaceable>filename</replaceable></arg>
<arg>-o</arg> <arg>-o</arg>
<arg>-u <replaceable>interface</replaceable></arg> <arg>-u <replaceable>interface</replaceable></arg>
@ -204,6 +206,8 @@ remoteuser = jgoerzen
and corporate networks do, and most operating systems and corporate networks do, and most operating systems
have an IMAP have an IMAP
implementation readily available. implementation readily available.
A special <property>Gmail</property> mailbox type is
available to interface with Gmail's IMAP front-end.
</para> </para>
</listitem> </listitem>
<listitem> <listitem>

119
offlineimap/folder/Gmail.py Normal file
View File

@ -0,0 +1,119 @@
# Gmail IMAP folder support
# Copyright (C) 2008 Riccardo Murri <riccardo.murri@gmail.com>
# Copyright (C) 2002-2007 John Goerzen <jgoerzen@complete.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., 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)

View File

@ -1,2 +1,2 @@
import Base, IMAP, Maildir, LocalStatus import Base, Gmail, IMAP, Maildir, LocalStatus

View File

@ -20,11 +20,13 @@ from offlineimap import CustomConfig
import os.path import os.path
def LoadRepository(name, account, reqtype): def LoadRepository(name, account, reqtype):
from offlineimap.repository.Gmail import GmailRepository
from offlineimap.repository.IMAP import IMAPRepository, MappedIMAPRepository from offlineimap.repository.IMAP import IMAPRepository, MappedIMAPRepository
from offlineimap.repository.Maildir import MaildirRepository from offlineimap.repository.Maildir import MaildirRepository
if reqtype == 'remote': if reqtype == 'remote':
# For now, we don't support Maildirs on the remote side. # For now, we don't support Maildirs on the remote side.
typemap = {'IMAP': IMAPRepository} typemap = {'IMAP': IMAPRepository,
'Gmail': GmailRepository}
elif reqtype == 'local': elif reqtype == 'local':
typemap = {'IMAP': MappedIMAPRepository, typemap = {'IMAP': MappedIMAPRepository,
'Maildir': MaildirRepository} 'Maildir': MaildirRepository}

View File

@ -0,0 +1,68 @@
# Gmail IMAP repository support
# Copyright (C) 2008 Riccardo Murri <riccardo.murri@gmail.com>
#
# 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)

View File

@ -1 +1 @@
__all__ = ['IMAP', 'Base', 'Maildir', 'LocalStatus'] __all__ = ['Gmail', 'IMAP', 'Base', 'Maildir', 'LocalStatus']