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]
|
[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
|
||||||
|
@ -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
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
|
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}
|
||||||
|
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