2012-10-16 20:20:35 +02:00
|
|
|
# Maildir folder support with labels
|
2016-06-29 03:42:57 +02:00
|
|
|
# Copyright (C) 2002-2016 John Goerzen & contributors.
|
2012-10-16 20:20:35 +02:00
|
|
|
#
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
2015-01-11 21:44:24 +01:00
|
|
|
from sys import exc_info
|
2012-10-16 20:20:35 +02:00
|
|
|
import offlineimap.accounts
|
2016-07-16 21:25:14 +02:00
|
|
|
from offlineimap import OfflineImapError
|
2012-11-28 18:29:23 +01:00
|
|
|
from offlineimap import imaputil
|
2020-08-29 21:10:16 +02:00
|
|
|
from .Maildir import MaildirFolder
|
2012-10-16 20:20:35 +02:00
|
|
|
|
2020-08-29 19:42:20 +02:00
|
|
|
|
2012-10-16 20:20:35 +02:00
|
|
|
class GmailMaildirFolder(MaildirFolder):
|
2016-07-16 21:25:14 +02:00
|
|
|
"""Folder implementation to support adding labels to messages in a Maildir."""
|
|
|
|
|
2012-10-16 20:20:35 +02:00
|
|
|
def __init__(self, root, name, sep, repository):
|
|
|
|
super(GmailMaildirFolder, self).__init__(root, name, sep, repository)
|
|
|
|
|
2016-07-16 21:25:14 +02:00
|
|
|
# The header under which labels are stored.
|
|
|
|
self.labelsheader = self.repository.account.getconf('labelsheader',
|
|
|
|
'X-Keywords')
|
2012-10-16 20:20:35 +02:00
|
|
|
|
2016-07-16 21:25:14 +02:00
|
|
|
# Enables / disables label sync.
|
2012-10-16 20:20:35 +02:00
|
|
|
self.synclabels = self.repository.account.getconfboolean('synclabels', 0)
|
|
|
|
|
2016-07-16 21:25:14 +02:00
|
|
|
# If synclabels is enabled, add a 4th pass to sync labels.
|
2012-10-16 20:20:35 +02:00
|
|
|
if self.synclabels:
|
2016-07-27 16:15:21 +02:00
|
|
|
self.syncmessagesto_passes.append(self.syncmessagesto_labels)
|
2012-10-16 20:20:35 +02:00
|
|
|
|
|
|
|
def quickchanged(self, statusfolder):
|
2016-07-16 21:25:14 +02:00
|
|
|
"""Returns True if the Maildir has changed.
|
|
|
|
|
|
|
|
Checks uids, flags and mtimes"""
|
2012-10-16 20:20:35 +02:00
|
|
|
|
2016-06-08 19:02:04 +02:00
|
|
|
if self._utime_from_header is True:
|
|
|
|
raise Exception("GmailMaildir does not support quick mode"
|
2020-08-29 19:42:20 +02:00
|
|
|
" when 'utime_from_header' is enabled.")
|
2016-06-08 19:02:04 +02:00
|
|
|
|
2012-10-16 20:20:35 +02:00
|
|
|
self.cachemessagelist()
|
2016-07-16 21:25:14 +02:00
|
|
|
# Folder has different uids than statusfolder => TRUE.
|
2012-10-16 20:20:35 +02:00
|
|
|
if sorted(self.getmessageuidlist()) != \
|
|
|
|
sorted(statusfolder.getmessageuidlist()):
|
|
|
|
return True
|
2016-07-16 21:25:14 +02:00
|
|
|
# Check for flag changes, it's quick on a Maildir.
|
2020-08-28 03:32:43 +02:00
|
|
|
for (uid, message) in list(self.getmessagelist().items()):
|
2012-10-16 20:20:35 +02:00
|
|
|
if message['flags'] != statusfolder.getmessageflags(uid):
|
|
|
|
return True
|
|
|
|
# check for newer mtimes. it is also fast
|
2020-08-28 03:32:43 +02:00
|
|
|
for (uid, message) in list(self.getmessagelist().items()):
|
2012-10-16 20:20:35 +02:00
|
|
|
if message['mtime'] > statusfolder.getmessagemtime(uid):
|
|
|
|
return True
|
2016-06-08 19:02:04 +02:00
|
|
|
return False # Nope, nothing changed.
|
2012-10-16 20:20:35 +02:00
|
|
|
|
2014-08-03 14:47:26 +02:00
|
|
|
# Interface from BaseFolder
|
|
|
|
def msglist_item_initializer(self, uid):
|
2014-08-22 15:43:36 +02:00
|
|
|
return {'flags': set(), 'labels': set(), 'labels_cached': False,
|
|
|
|
'filename': '/no-dir/no-such-file/', 'mtime': 0}
|
2014-08-03 14:47:26 +02:00
|
|
|
|
maxage: fix timezone issues, remove IMAP-IMAP support, add startdate option
1. When using maxage, local and remote messagelists are supposed to only
contain messages from at most maxage days ago. But local and remote used
different timezones to calculate what "maxage days ago" means, resulting
in removals on one side. Now, we ask the local folder for maxage days'
worth of mail, find the lowest UID, and then ask the remote folder for
all UID's starting with that lowest one.
2. maxage was fundamentally wrong in the IMAP-IMAP case: it assumed that
remote messages have UIDs in the same order as their local counterparts,
which could be false, e.g. when messages are copied in quick succession.
So, remove support for maxage in the IMAP-IMAP case.
3. Add startdate option for IMAP-IMAP syncs: use messages from the given
repository starting at startdate, and all messages from the other
repository. In the first sync, the other repository must be empty.
4. Allow maxage to be specified either as number of days to sync (as
previously) or as a fixed date.
Signed-off-by: Janna Martl <janna.martl109@gmail.com>
Signed-off-by: Nicolas Sebrecht <nicolas.s-dev@laposte.net>
2015-04-07 10:14:11 +02:00
|
|
|
def cachemessagelist(self, min_date=None, min_uid=None):
|
2015-02-13 17:02:33 +01:00
|
|
|
if self.ismessagelistempty():
|
2016-07-16 21:25:14 +02:00
|
|
|
self.messagelist = self._scanfolder(min_date=min_date,
|
|
|
|
min_uid=min_uid)
|
2012-10-16 20:20:35 +02:00
|
|
|
|
|
|
|
# Get mtimes
|
|
|
|
if self.synclabels:
|
2016-05-16 19:48:50 +02:00
|
|
|
for uid, msg in list(self.messagelist.items()):
|
2012-10-16 20:20:35 +02:00
|
|
|
filepath = os.path.join(self.getfullname(), msg['filename'])
|
2016-05-08 16:42:52 +02:00
|
|
|
msg['mtime'] = int(os.stat(filepath).st_mtime)
|
2012-10-16 20:20:35 +02:00
|
|
|
|
|
|
|
def getmessagelabels(self, uid):
|
|
|
|
# Labels are not cached in cachemessagelist because it is too slow.
|
2014-08-22 15:43:36 +02:00
|
|
|
if not self.messagelist[uid]['labels_cached']:
|
2012-10-16 20:20:35 +02:00
|
|
|
filename = self.messagelist[uid]['filename']
|
|
|
|
filepath = os.path.join(self.getfullname(), filename)
|
|
|
|
|
|
|
|
if not os.path.exists(filepath):
|
|
|
|
return set()
|
|
|
|
|
|
|
|
file = open(filepath, 'rt')
|
|
|
|
content = file.read()
|
|
|
|
file.close()
|
|
|
|
|
2014-11-20 14:03:15 +01:00
|
|
|
self.messagelist[uid]['labels'] = set()
|
|
|
|
for hstr in self.getmessageheaderlist(content, self.labelsheader):
|
|
|
|
self.messagelist[uid]['labels'].update(
|
|
|
|
imaputil.labels_from_header(self.labelsheader, hstr))
|
2014-08-22 15:43:36 +02:00
|
|
|
self.messagelist[uid]['labels_cached'] = True
|
2012-10-16 20:20:35 +02:00
|
|
|
|
|
|
|
return self.messagelist[uid]['labels']
|
|
|
|
|
|
|
|
def getmessagemtime(self, uid):
|
2020-08-30 13:23:07 +02:00
|
|
|
if 'mtime' not in self.messagelist[uid]:
|
2012-10-16 20:20:35 +02:00
|
|
|
return 0
|
|
|
|
else:
|
|
|
|
return self.messagelist[uid]['mtime']
|
|
|
|
|
|
|
|
def savemessage(self, uid, content, flags, rtime):
|
|
|
|
"""Writes a new message, with the specified uid.
|
|
|
|
|
|
|
|
See folder/Base for detail. Note that savemessage() does not
|
|
|
|
check against dryrun settings, so you need to ensure that
|
|
|
|
savemessage is never called in a dryrun mode."""
|
|
|
|
|
|
|
|
if not self.synclabels:
|
2016-07-16 21:25:14 +02:00
|
|
|
return super(GmailMaildirFolder, self).savemessage(uid, content,
|
|
|
|
flags, rtime)
|
2012-10-16 20:20:35 +02:00
|
|
|
|
2014-11-20 14:03:15 +01:00
|
|
|
labels = set()
|
|
|
|
for hstr in self.getmessageheaderlist(content, self.labelsheader):
|
|
|
|
labels.update(imaputil.labels_from_header(self.labelsheader, hstr))
|
|
|
|
|
2016-07-16 21:25:14 +02:00
|
|
|
ret = super(GmailMaildirFolder, self).savemessage(uid, content, flags,
|
|
|
|
rtime)
|
2012-10-16 20:20:35 +02:00
|
|
|
|
2016-07-16 21:25:14 +02:00
|
|
|
# Update the mtime and labels.
|
2012-10-16 20:20:35 +02:00
|
|
|
filename = self.messagelist[uid]['filename']
|
|
|
|
filepath = os.path.join(self.getfullname(), filename)
|
2016-05-08 16:42:52 +02:00
|
|
|
self.messagelist[uid]['mtime'] = int(os.stat(filepath).st_mtime)
|
2012-10-16 20:20:35 +02:00
|
|
|
self.messagelist[uid]['labels'] = labels
|
|
|
|
return ret
|
|
|
|
|
2020-10-10 15:00:34 +02:00
|
|
|
def savemessagelabels(self, uid, labels, ignorelabels=None):
|
2012-10-16 20:20:35 +02:00
|
|
|
"""Change a message's labels to `labels`.
|
|
|
|
|
|
|
|
Note that this function does not check against dryrun settings,
|
|
|
|
so you need to ensure that it is never called in a dryrun mode."""
|
|
|
|
|
2020-10-10 15:00:34 +02:00
|
|
|
if ignorelabels is None:
|
|
|
|
ignorelabels = set()
|
|
|
|
|
2012-10-16 20:20:35 +02:00
|
|
|
filename = self.messagelist[uid]['filename']
|
|
|
|
filepath = os.path.join(self.getfullname(), filename)
|
|
|
|
|
|
|
|
file = open(filepath, 'rt')
|
|
|
|
content = file.read()
|
|
|
|
file.close()
|
|
|
|
|
2014-11-20 14:03:15 +01:00
|
|
|
oldlabels = set()
|
|
|
|
for hstr in self.getmessageheaderlist(content, self.labelsheader):
|
2016-07-16 21:25:14 +02:00
|
|
|
oldlabels.update(imaputil.labels_from_header(self.labelsheader,
|
|
|
|
hstr))
|
2012-10-16 20:20:35 +02:00
|
|
|
|
|
|
|
labels = labels - ignorelabels
|
|
|
|
ignoredlabels = oldlabels & ignorelabels
|
|
|
|
oldlabels = oldlabels - ignorelabels
|
|
|
|
|
2016-07-16 21:25:14 +02:00
|
|
|
# Nothing to change.
|
2012-10-16 20:20:35 +02:00
|
|
|
if labels == oldlabels:
|
|
|
|
return
|
|
|
|
|
2016-07-16 21:25:14 +02:00
|
|
|
# Change labels into content.
|
2012-11-28 18:29:23 +01:00
|
|
|
labels_str = imaputil.format_labels_string(self.labelsheader,
|
2020-08-29 19:42:20 +02:00
|
|
|
sorted(labels | ignoredlabels))
|
2014-11-20 14:16:48 +01:00
|
|
|
|
2016-07-16 21:25:14 +02:00
|
|
|
# First remove old labels header, and then add the new one.
|
2014-11-20 14:16:48 +01:00
|
|
|
content = self.deletemessageheaders(content, self.labelsheader)
|
2016-07-16 21:25:14 +02:00
|
|
|
content = self.addmessageheader(content, '\n', self.labelsheader,
|
|
|
|
labels_str)
|
2014-11-20 14:16:48 +01:00
|
|
|
|
2016-05-08 16:42:52 +02:00
|
|
|
mtime = int(os.stat(filepath).st_mtime)
|
2012-10-16 20:20:35 +02:00
|
|
|
|
2016-07-16 21:25:14 +02:00
|
|
|
# Write file with new labels to a unique file in tmp.
|
2012-10-16 20:20:35 +02:00
|
|
|
messagename = self.new_message_filename(uid, set())
|
2012-11-28 18:29:23 +01:00
|
|
|
tmpname = self.save_to_tmp_file(messagename, content)
|
2012-10-16 20:20:35 +02:00
|
|
|
tmppath = os.path.join(self.getfullname(), tmpname)
|
|
|
|
|
2016-07-16 21:25:14 +02:00
|
|
|
# Move to actual location.
|
2012-10-16 20:20:35 +02:00
|
|
|
try:
|
|
|
|
os.rename(tmppath, filepath)
|
|
|
|
except OSError as e:
|
2020-09-03 20:38:05 +02:00
|
|
|
raise OfflineImapError("Can't rename file '%s' to '%s': %s" %
|
|
|
|
(tmppath, filepath, e[1]),
|
|
|
|
OfflineImapError.ERROR.FOLDER,
|
|
|
|
exc_info()[2])
|
2012-10-16 20:20:35 +02:00
|
|
|
|
2016-07-16 21:25:14 +02:00
|
|
|
# If utime_from_header=true, we don't want to change the mtime.
|
|
|
|
if self._utime_from_header and mtime:
|
2015-04-03 03:29:36 +02:00
|
|
|
os.utime(filepath, (mtime, mtime))
|
2012-10-16 20:20:35 +02:00
|
|
|
|
|
|
|
# save the new mtime and labels
|
2016-05-08 16:42:52 +02:00
|
|
|
self.messagelist[uid]['mtime'] = int(os.stat(filepath).st_mtime)
|
2012-10-16 20:20:35 +02:00
|
|
|
self.messagelist[uid]['labels'] = labels
|
|
|
|
|
2016-07-16 21:25:14 +02:00
|
|
|
def copymessageto(self, uid, dstfolder, statusfolder, register=1):
|
2012-10-16 20:20:35 +02:00
|
|
|
"""Copies a message from self to dst if needed, updating the status
|
|
|
|
|
|
|
|
Note that this function does not check against dryrun settings,
|
|
|
|
so you need to ensure that it is never called in a
|
|
|
|
dryrun mode.
|
|
|
|
|
|
|
|
:param uid: uid of the message to be copied.
|
|
|
|
:param dstfolder: A BaseFolder-derived instance
|
|
|
|
:param statusfolder: A LocalStatusFolder instance
|
|
|
|
:param register: whether we should register a new thread."
|
|
|
|
:returns: Nothing on success, or raises an Exception."""
|
|
|
|
|
2016-07-16 21:25:14 +02:00
|
|
|
# Check if we are really copying.
|
2012-10-16 20:20:35 +02:00
|
|
|
realcopy = uid > 0 and not dstfolder.uidexists(uid)
|
|
|
|
|
2016-07-16 21:25:14 +02:00
|
|
|
# First copy the message.
|
|
|
|
super(GmailMaildirFolder, self).copymessageto(uid, dstfolder,
|
2020-08-29 19:42:20 +02:00
|
|
|
statusfolder, register)
|
2012-10-16 20:20:35 +02:00
|
|
|
|
2016-07-16 21:25:14 +02:00
|
|
|
# Sync labels and mtime now when the message is new (the embedded labels
|
|
|
|
# are up to date, and have already propagated to the remote server. For
|
|
|
|
# message which already existed on the remote, this is useless, as later
|
|
|
|
# the labels may get updated.
|
2012-10-16 20:20:35 +02:00
|
|
|
if realcopy and self.synclabels:
|
|
|
|
try:
|
|
|
|
labels = dstfolder.getmessagelabels(uid)
|
2016-07-16 21:25:14 +02:00
|
|
|
statusfolder.savemessagelabels(uid, labels,
|
2020-08-29 19:42:20 +02:00
|
|
|
mtime=self.getmessagemtime(uid))
|
2012-10-16 20:20:35 +02:00
|
|
|
|
2013-07-27 23:25:13 +02:00
|
|
|
# dstfolder is not GmailMaildir.
|
2012-10-16 20:20:35 +02:00
|
|
|
except NotImplementedError:
|
|
|
|
return
|
|
|
|
|
|
|
|
def syncmessagesto_labels(self, dstfolder, statusfolder):
|
|
|
|
"""Pass 4: Label Synchronization (Gmail only)
|
|
|
|
|
|
|
|
Compare label mismatches in self with those in statusfolder. If
|
|
|
|
msg has a valid UID and exists on dstfolder (has not e.g. been
|
|
|
|
deleted there), sync the labels change to both dstfolder and
|
|
|
|
statusfolder.
|
|
|
|
|
|
|
|
Also skips messages whose mtime remains the same as statusfolder, as the
|
|
|
|
contents have not changed.
|
|
|
|
|
|
|
|
This function checks and protects us from action in ryrun mode.
|
|
|
|
"""
|
|
|
|
# For each label, we store a list of uids to which it should be
|
|
|
|
# added. Then, we can call addmessageslabels() to apply them in
|
|
|
|
# bulk, rather than one call per message.
|
|
|
|
addlabellist = {}
|
|
|
|
dellabellist = {}
|
|
|
|
uidlist = []
|
|
|
|
|
|
|
|
try:
|
2016-07-16 21:25:14 +02:00
|
|
|
# Filter uids (fast).
|
2012-10-16 20:20:35 +02:00
|
|
|
for uid in self.getmessageuidlist():
|
2016-07-16 21:25:14 +02:00
|
|
|
# Bail out on CTRL-C or SIGTERM.
|
2012-10-16 20:20:35 +02:00
|
|
|
if offlineimap.accounts.Account.abort_NOW_signal.is_set():
|
|
|
|
break
|
|
|
|
|
|
|
|
# Ignore messages with negative UIDs missed by pass 1 and
|
2016-07-16 21:25:14 +02:00
|
|
|
# don't do anything if the message has been deleted remotely.
|
2012-10-16 20:20:35 +02:00
|
|
|
if uid < 0 or not dstfolder.uidexists(uid):
|
|
|
|
continue
|
|
|
|
|
|
|
|
selfmtime = self.getmessagemtime(uid)
|
|
|
|
|
|
|
|
if statusfolder.uidexists(uid):
|
|
|
|
statusmtime = statusfolder.getmessagemtime(uid)
|
|
|
|
else:
|
|
|
|
statusmtime = 0
|
|
|
|
|
|
|
|
if selfmtime > statusmtime:
|
|
|
|
uidlist.append(uid)
|
|
|
|
|
|
|
|
self.ui.collectingdata(uidlist, self)
|
2016-07-16 21:25:14 +02:00
|
|
|
# This can be slow if there is a lot of modified files.
|
2012-10-16 20:20:35 +02:00
|
|
|
for uid in uidlist:
|
2016-07-16 21:25:14 +02:00
|
|
|
# Bail out on CTRL-C or SIGTERM.
|
2012-10-16 20:20:35 +02:00
|
|
|
if offlineimap.accounts.Account.abort_NOW_signal.is_set():
|
|
|
|
break
|
|
|
|
|
|
|
|
selflabels = self.getmessagelabels(uid)
|
|
|
|
|
|
|
|
if statusfolder.uidexists(uid):
|
|
|
|
statuslabels = statusfolder.getmessagelabels(uid)
|
|
|
|
else:
|
|
|
|
statuslabels = set()
|
|
|
|
|
|
|
|
addlabels = selflabels - statuslabels
|
|
|
|
dellabels = statuslabels - selflabels
|
|
|
|
|
|
|
|
for lb in addlabels:
|
2020-08-30 13:23:07 +02:00
|
|
|
if lb not in addlabellist:
|
2012-10-16 20:20:35 +02:00
|
|
|
addlabellist[lb] = []
|
|
|
|
addlabellist[lb].append(uid)
|
|
|
|
|
|
|
|
for lb in dellabels:
|
2020-08-30 13:23:07 +02:00
|
|
|
if lb not in dellabellist:
|
2012-10-16 20:20:35 +02:00
|
|
|
dellabellist[lb] = []
|
|
|
|
dellabellist[lb].append(uid)
|
|
|
|
|
2020-08-28 03:32:43 +02:00
|
|
|
for lb, uids in list(addlabellist.items()):
|
2016-07-16 21:25:14 +02:00
|
|
|
# Bail out on CTRL-C or SIGTERM.
|
2012-10-16 20:20:35 +02:00
|
|
|
if offlineimap.accounts.Account.abort_NOW_signal.is_set():
|
|
|
|
break
|
|
|
|
|
|
|
|
self.ui.addinglabels(uids, lb, dstfolder)
|
|
|
|
if self.repository.account.dryrun:
|
2020-08-29 19:42:20 +02:00
|
|
|
continue # Don't actually add in a dryrun.
|
2012-10-16 20:20:35 +02:00
|
|
|
dstfolder.addmessageslabels(uids, set([lb]))
|
|
|
|
statusfolder.addmessageslabels(uids, set([lb]))
|
|
|
|
|
2020-08-28 03:32:43 +02:00
|
|
|
for lb, uids in list(dellabellist.items()):
|
2016-07-16 21:25:14 +02:00
|
|
|
# Bail out on CTRL-C or SIGTERM.
|
2012-10-16 20:20:35 +02:00
|
|
|
if offlineimap.accounts.Account.abort_NOW_signal.is_set():
|
|
|
|
break
|
|
|
|
|
|
|
|
self.ui.deletinglabels(uids, lb, dstfolder)
|
|
|
|
if self.repository.account.dryrun:
|
2020-08-29 19:42:20 +02:00
|
|
|
continue # Don't actually remove in a dryrun.
|
2012-10-16 20:20:35 +02:00
|
|
|
dstfolder.deletemessageslabels(uids, set([lb]))
|
|
|
|
statusfolder.deletemessageslabels(uids, set([lb]))
|
|
|
|
|
2016-07-16 21:25:14 +02:00
|
|
|
# Update mtimes on StatusFolder. It is done last to be safe. If
|
|
|
|
# something els fails and the mtime is not updated, the labels will
|
|
|
|
# still be synced next time.
|
2012-10-16 20:20:35 +02:00
|
|
|
mtimes = {}
|
|
|
|
for uid in uidlist:
|
2016-07-16 21:25:14 +02:00
|
|
|
# Bail out on CTRL-C or SIGTERM.
|
2012-10-16 20:20:35 +02:00
|
|
|
if offlineimap.accounts.Account.abort_NOW_signal.is_set():
|
|
|
|
break
|
|
|
|
|
|
|
|
if self.repository.account.dryrun:
|
2020-08-29 19:42:20 +02:00
|
|
|
continue # Don't actually update statusfolder.
|
2012-10-16 20:20:35 +02:00
|
|
|
|
|
|
|
filename = self.messagelist[uid]['filename']
|
|
|
|
filepath = os.path.join(self.getfullname(), filename)
|
2016-05-08 16:42:52 +02:00
|
|
|
mtimes[uid] = int(os.stat(filepath).st_mtime)
|
2012-10-16 20:20:35 +02:00
|
|
|
|
2016-07-16 21:25:14 +02:00
|
|
|
# Finally, update statusfolder in a single DB transaction.
|
2012-10-16 20:20:35 +02:00
|
|
|
statusfolder.savemessagesmtimebulk(mtimes)
|
|
|
|
|
|
|
|
except NotImplementedError:
|
2016-07-16 21:25:14 +02:00
|
|
|
self.ui.warn("Can't sync labels. You need to configure a remote "
|
|
|
|
"repository of type Gmail.")
|