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:
parent
ec89c3eb53
commit
81b86fb74c
@ -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
|
||||
|
@ -32,6 +32,8 @@
|
||||
<arg>-a <replaceable>accountlist</replaceable></arg>
|
||||
<arg>-c <replaceable>configfile</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>-o</arg>
|
||||
<arg>-u <replaceable>interface</replaceable></arg>
|
||||
@ -204,6 +206,8 @@ remoteuser = jgoerzen
|
||||
and corporate networks do, and most operating systems
|
||||
have an IMAP
|
||||
implementation readily available.
|
||||
A special <property>Gmail</property> mailbox type is
|
||||
available to interface with Gmail's IMAP front-end.
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
|
119
offlineimap/folder/Gmail.py
Normal file
119
offlineimap/folder/Gmail.py
Normal 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)
|
@ -1,2 +1,2 @@
|
||||
import Base, IMAP, Maildir, LocalStatus
|
||||
import Base, Gmail, IMAP, Maildir, LocalStatus
|
||||
|
||||
|
@ -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}
|
||||
|
68
offlineimap/repository/Gmail.py
Normal file
68
offlineimap/repository/Gmail.py
Normal 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)
|
@ -1 +1 @@
|
||||
__all__ = ['IMAP', 'Base', 'Maildir', 'LocalStatus']
|
||||
__all__ = ['Gmail', 'IMAP', 'Base', 'Maildir', 'LocalStatus']
|
||||
|
Loading…
Reference in New Issue
Block a user