Merge pull request #22 from thekix/master

Multiple style patches and bug about remotepasseval
This commit is contained in:
Rodolfo García Peñas (kix) 2020-11-04 22:20:43 +01:00 committed by GitHub
commit 319e5a03b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 744 additions and 327 deletions

View File

@ -1,19 +1,21 @@
# Base folder support
# Copyright (C) 2002-2016 John Goerzen & contributors
#
# 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
"""
Base folder support
Copyright (C) 2002-2016 John Goerzen & contributors
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.path
import re
@ -27,6 +29,9 @@ import offlineimap.accounts
class BaseFolder:
"""
Base Folder Class
"""
__hash__ = None
def __init__(self, name, repository):
@ -130,6 +135,12 @@ class BaseFolder:
return self.repository.should_sync_folder(self.ffilter_name)
def dofsync(self):
"""
Call and returns _dofsync()
Returns: Call and returns _dofsync()
"""
return self._dofsync
def suggeststhreads(self):
@ -196,6 +207,13 @@ class BaseFolder:
return self.sep
def getfullname(self):
"""
Returns the folder full name, using the getname(). If getroot() is set
their value is concatenated to getname() using the separator
Returns: The folder full name
"""
if self.getroot():
return self.getroot() + self.getsep() + self.getname()
else:
@ -371,6 +389,12 @@ class BaseFolder:
OfflineImapError.ERROR.MESSAGE)
def getmaxsize(self):
"""
Get the maxsize for account name. If not found, returns None.
Returns: A string with the maxise of the account name
"""
return self.config.getdefaultint("Account %s" %
self.accountname, "maxsize", None)
@ -396,19 +420,41 @@ class BaseFolder:
OfflineImapError.ERROR.MESSAGE)
def get_min_uid_file(self):
"""
Get the min UID file name. Create it if not found.
Returns: Min UID file name.
"""
startuiddir = os.path.join(self.config.getmetadatadir(),
'Repository-' + self.repository.name, 'StartUID')
'Repository-' + self.repository.name,
'StartUID')
if not os.path.exists(startuiddir):
os.mkdir(startuiddir, 0o700)
return os.path.join(startuiddir, self.getfolderbasename())
def save_min_uid(self, min_uid):
"""
Save the min UID in the min uid file
Args:
min_uid: min_uid to save
Returns: None
"""
uidfile = self.get_min_uid_file()
fd = open(uidfile, 'wt')
fd.write(str(min_uid) + "\n")
fd.close()
def retrieve_min_uid(self):
"""
Retrieve the min UID file
Returns: min UID of file
"""
uidfile = self.get_min_uid_file()
if not os.path.exists(uidfile):
return None
@ -594,8 +640,8 @@ class BaseFolder:
def addmessageheader(self, content, linebreak, headername, headervalue):
"""Adds new header to the provided message.
WARNING: This function is a bit tricky, and modifying it in the wrong way,
may easily lead to data-loss.
WARNING: This function is a bit tricky, and modifying it in the wrong
way, may easily lead to data-loss.
Arguments:
- content: message content, headers and body as a single string
@ -605,8 +651,8 @@ class BaseFolder:
.. note::
The following documentation will not get displayed correctly after being
processed by Sphinx. View the source of this method to read it.
The following documentation will not get displayed correctly after
being processed by Sphinx. View the source of this method to read it.
This has to deal with strange corner cases where the header is
missing or empty. Here are illustrations for all the cases,
@ -659,7 +705,8 @@ class BaseFolder:
self.ui.debug('', 'addmessageheader: headers were missing')
else:
self.ui.debug('',
'addmessageheader: headers end at position %d' % insertionpoint)
'addmessageheader: headers end at position %d' %
insertionpoint)
mark = '==>EOH<=='
contextstart = max(0, insertionpoint - 100)
contextend = min(len(content), insertionpoint + 100)
@ -693,9 +740,11 @@ class BaseFolder:
self.ui.debug('',
'addmessageheader: insertionpoint = %d' % insertionpoint)
headers = content[0:insertionpoint]
self.ui.debug('', 'addmessageheader: headers = %s' % repr(headers))
self.ui.debug('',
'addmessageheader: headers = %s' % repr(headers))
new_header = prefix + ("%s: %s" % (headername, headervalue)) + suffix
self.ui.debug('', 'addmessageheader: new_header = %s' % repr(new_header))
self.ui.debug('',
'addmessageheader: new_header = %s' % repr(new_header))
return headers + new_header + content[insertionpoint:]
def __find_eoh(self, content):
@ -746,7 +795,8 @@ class BaseFolder:
- contents: message itself
- name: name of the header to be searched
Returns: list of header values or empty list if no such header was found.
Returns: list of header values or empty list if no such header was
found.
"""
self.ui.debug('', 'getmessageheaderlist: called to get %s' % name)
@ -769,7 +819,8 @@ class BaseFolder:
if type(header_list) != type([]):
header_list = [header_list]
self.ui.debug('', 'deletemessageheaders: called to delete %s' % header_list)
self.ui.debug('',
'deletemessageheaders: called to delete %s' % header_list)
if not len(header_list):
return content
@ -783,7 +834,8 @@ class BaseFolder:
for h in headers.split('\n'):
keep_it = True
for trim_h in header_list:
if len(h) > len(trim_h) and h[0:len(trim_h) + 1] == (trim_h + ":"):
if len(h) > len(trim_h) \
and h[0:len(trim_h) + 1] == (trim_h + ":"):
keep_it = False
break
if keep_it:
@ -873,9 +925,10 @@ class BaseFolder:
# IMAP servers ...
self.deletemessage(uid)
else:
raise OfflineImapError("Trying to save msg (uid %d) on folder "
"%s returned invalid uid %d" % (uid, dstfolder.getvisiblename(),
new_uid), OfflineImapError.ERROR.MESSAGE)
msg = "Trying to save msg (uid %d) on folder " \
"%s returned invalid uid %d" % \
(uid, dstfolder.getvisiblename(), new_uid)
raise OfflineImapError(msg, OfflineImapError.ERROR.MESSAGE)
except KeyboardInterrupt: # Bubble up CTRL-C.
raise
except OfflineImapError as e:
@ -884,7 +937,8 @@ class BaseFolder:
self.ui.error(e, exc_info()[2])
except Exception as e:
self.ui.error(e, exc_info()[2],
msg="Copying message %s [acc: %s]" % (uid, self.accountname))
msg="Copying message %s [acc: %s]" %
(uid, self.accountname))
raise # Raise on unknown errors, so we can fix those.
def __syncmessagesto_copy(self, dstfolder, statusfolder):
@ -929,24 +983,28 @@ class BaseFolder:
break
if uid == 0:
self.ui.warn("Assertion that UID != 0 failed; ignoring message.")
msg = "Assertion that UID != 0 failed; ignoring message."
self.ui.warn(msg)
continue
if uid > 0 and dstfolder.uidexists(uid):
# dstfolder has message with that UID already, only update status.
# dstfolder has message with that UID already,
# only update status.
flags = self.getmessageflags(uid)
rtime = self.getmessagetime(uid)
statusfolder.savemessage(uid, None, flags, rtime)
continue
self.ui.copyingmessage(uid, num + 1, num_to_copy, self, dstfolder)
self.ui.copyingmessage(uid, num + 1, num_to_copy, self,
dstfolder)
# Exceptions are caught in copymessageto().
if self.suggeststhreads():
self.waitforthread()
thread = threadutil.InstanceLimitedThread(
self.getinstancelimitnamespace(),
target=self.copymessageto,
name="Copy message from %s:%s" % (self.repository, self),
name="Copy message from %s:%s" % (self.repository,
self),
args=(uid, dstfolder, statusfolder)
)
thread.start()
@ -1015,9 +1073,10 @@ class BaseFolder:
# skip them.
skipped_keywords = list(selfkeywords - knownkeywords)
selfkeywords &= knownkeywords
self.ui.warn("Unknown keywords skipped: %s\n"
"You may want to change your configuration to include "
"those\n" % skipped_keywords)
msg = "Unknown keywords skipped: %s\n" \
"You may want to change your configuration to include " \
"those\n" % skipped_keywords
self.ui.warn(msg)
keywordletterset = set([keywordmap[keyw] for keyw in selfkeywords])
@ -1125,16 +1184,16 @@ class BaseFolder:
break
try:
action(dstfolder, statusfolder)
except (KeyboardInterrupt):
except KeyboardInterrupt:
raise
except OfflineImapError as e:
if e.severity > OfflineImapError.ERROR.FOLDER:
raise
self.ui.error(e, exc_info()[2], "while syncing %s [account %s]" %
(self, self.accountname))
msg = "while syncing %s [account %s]" % (self, self.accountname)
self.ui.error(e, exc_info()[2], msg)
except Exception as e:
self.ui.error(e, exc_info()[2], "while syncing %s [account %s]" %
(self, self.accountname))
msg = "while syncing %s [account %s]" % (self, self.accountname)
self.ui.error(e, exc_info()[2], msg)
raise # Raise unknown Exceptions so we can fix them.
def __eq__(self, other):

View File

@ -1 +1,4 @@
"""
Folder Module of offlineimap
"""
from . import Base, Gmail, IMAP, Maildir, LocalStatus, UIDMaps

View File

@ -18,9 +18,10 @@
import datetime
import hashlib
import hmac
import socket
import json
import urllib.request, urllib.parse, urllib.error
import urllib.request
import urllib.parse
import urllib.error
import time
import errno
import socket
@ -39,7 +40,7 @@ except ImportError:
have_gss = False
class IMAPServer():
class IMAPServer:
"""Initializes all variables from an IMAPRepository() instance
Various functions, such as acquireconnection() return an IMAP4
@ -331,7 +332,8 @@ class IMAPServer():
except imapobj.error as e:
raise OfflineImapError("Failed to start "
"TLS connection: %s" % str(e),
OfflineImapError.ERROR.REPO, None, exc_info()[2])
OfflineImapError.ERROR.REPO,
exc_info()[2])
# All __authn_* procedures are helpers that do authentication.
# They are class methods that take one parameter, IMAP object.
@ -776,7 +778,7 @@ class IdleThread:
self.thread = Thread(target=self.noop)
else:
self.thread = Thread(target=self.__idle)
self.thread.setDaemon(1)
self.thread.setDaemon(True)
def start(self):
self.thread.start()

View File

@ -1,21 +1,20 @@
""" Base repository support """
""" Base repository support
Copyright (C) 2002-2017 John Goerzen & contributors
# Copyright (C) 2002-2017 John Goerzen & contributors
#
# 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
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 re
import os.path
from sys import exc_info
@ -26,6 +25,9 @@ from offlineimap.error import OfflineImapError
class BaseRepository(CustomConfig.ConfigHelperMixin):
"""
Base Class for Repository
"""
def __init__(self, reposname, account):
self.ui = getglobalui()
self.account = account
@ -34,7 +36,8 @@ class BaseRepository(CustomConfig.ConfigHelperMixin):
self.localeval = account.getlocaleval()
self._accountname = self.account.getname()
self._readonly = self.getconfboolean('readonly', False)
self.uiddir = os.path.join(self.config.getmetadatadir(), 'Repository-' + self.name)
self.uiddir = os.path.join(self.config.getmetadatadir(),
'Repository-' + self.name)
if not os.path.exists(self.uiddir):
os.mkdir(self.uiddir, 0o700)
self.mapdir = os.path.join(self.uiddir, 'UIDMapping')
@ -70,8 +73,6 @@ class BaseRepository(CustomConfig.ConfigHelperMixin):
False), applies only to local Maildir mailboxes and does nothing
on all other repository types."""
pass
def connect(self):
"""Establish a connection to the remote, if necessary. This exists
so that IMAP connections can all be established up front, gathering
@ -80,18 +81,38 @@ class BaseRepository(CustomConfig.ConfigHelperMixin):
trap in order to validate the password, and that's the point of
this function."""
pass
def holdordropconnections(self):
pass
"""
Hold the drop connections functions.
Returns: None
"""
def dropconnections(self):
pass
"""
Drop connections functions.
Returns: None
"""
def getaccount(self):
"""
This patch returns the account
Returns: The account
"""
return self.account
def getname(self):
"""
Get the repository name
Returns: String with the repository name
"""
return self.name
def __str__(self):
@ -104,9 +125,21 @@ class BaseRepository(CustomConfig.ConfigHelperMixin):
return self._accountname
def getuiddir(self):
"""
The FolderValidity directory
Returns: The FolderValidity directory
"""
return self.uiddir
def getmapdir(self):
"""
Get the map dir (UIDMapping)
Returns: The UIDMapping directory
"""
return self.mapdir
# Interface from CustomConfig.ConfigHelperMixin
@ -124,6 +157,12 @@ class BaseRepository(CustomConfig.ConfigHelperMixin):
return self._readonly
def getlocaleval(self):
"""
Get the account local eval.
Returns: LocalEval class for account.
"""
return self.account.getlocaleval()
def getfolders(self):
@ -135,12 +174,26 @@ class BaseRepository(CustomConfig.ConfigHelperMixin):
"""Forgets the cached list of folders, if any. Useful to run
after a sync run."""
pass
def getsep(self):
"""
Get the separator.
This function is not implemented.
Returns: None
"""
raise NotImplementedError
def getkeywordmap(self):
"""
Get the keyword map.
This function is not implemented.
Returns: None
"""
raise NotImplementedError
def should_sync_folder(self, fname):
@ -154,14 +207,34 @@ class BaseRepository(CustomConfig.ConfigHelperMixin):
It is disabled by either setting the whole repository
'readonly' or by using the 'createfolders' setting."""
return (not self._readonly) and self.getconfboolean('createfolders', True)
return (not self._readonly) and self.getconfboolean('createfolders',
True)
def makefolder(self, foldername):
"""Create a new folder."""
"""
Create a new folder.
This function is not implemented
Args:
foldername: Folder to create
Returns: None
"""
raise NotImplementedError
def deletefolder(self, foldername):
"""
Remove the selected folder.
This function is not implemented
Args:
foldername: Folder to delete
Returns: None
"""
raise NotImplementedError
def getfolder(self, foldername, decode=True):
@ -182,9 +255,10 @@ class BaseRepository(CustomConfig.ConfigHelperMixin):
Configuring nametrans on BOTH repositories could lead to infinite folder
creation cycles."""
if not self.should_create_folders() and not local_repo.should_create_folders():
if not self.should_create_folders()\
and not local_repo.should_create_folders():
# Quick exit if no folder creation is enabled on either side.
return
return None
remote_repo = self
remote_hash, local_hash = {}, {}
@ -204,13 +278,14 @@ class BaseRepository(CustomConfig.ConfigHelperMixin):
# Apply remote nametrans and fix serparator.
local_name = remote_folder.getvisiblename().replace(
remote_repo.getsep(), local_repo.getsep())
if remote_folder.sync_this and local_name not in list(local_hash.keys()):
if remote_folder.sync_this \
and local_name not in list(local_hash.keys()):
try:
local_repo.makefolder(local_name)
# Need to refresh list.
local_repo.forgetfolders()
except OfflineImapError as e:
self.ui.error(e, exc_info()[2],
except OfflineImapError as exc:
self.ui.error(exc, exc_info()[2],
"Creating folder %s on repository %s" %
(local_name, local_repo))
raise
@ -226,13 +301,15 @@ class BaseRepository(CustomConfig.ConfigHelperMixin):
# Apply reverse nametrans and fix serparator.
remote_name = local_folder.getvisiblename().replace(
local_repo.getsep(), remote_repo.getsep())
if local_folder.sync_this and remote_name not in list(remote_hash.keys()):
if local_folder.sync_this \
and remote_name not in list(remote_hash.keys()):
# Would the remote filter out the new folder name? In this case
# don't create it.
if not remote_repo.should_sync_folder(remote_name):
self.ui.debug('', "Not creating folder '%s' (repository '%s"
"') as it would be filtered out on that repository." %
(remote_name, self))
msg = "Not creating folder '%s' (repository '%s') " \
"as it would be filtered out on that repository." % \
(remote_name, self)
self.ui.debug('', msg)
continue
# nametrans sanity check! Does remote nametrans lead to the
@ -247,32 +324,35 @@ class BaseRepository(CustomConfig.ConfigHelperMixin):
# Get IMAPFolder and see if the reverse nametrans works fine.
# TODO: getfolder() works only because we succeed in getting
# inexisting folders which I would like to change. Take care!
tmp_remotefolder = remote_repo.getfolder(remote_name, decode=False)
tmp_remotefolder = remote_repo.getfolder(remote_name,
decode=False)
loop_name = tmp_remotefolder.getvisiblename().replace(
remote_repo.getsep(), local_repo.getsep())
if local_name != loop_name:
raise OfflineImapError("INFINITE FOLDER CREATION DETECTED! "
"Folder '%s' (repository '%s') would be created as fold"
"er '%s' (repository '%s'). The latter becomes '%s' in "
"return, leading to infinite folder creation cycles.\n "
"SOLUTION: 1) Do set your nametrans rules on both repos"
"itories so they lead to identical names if applied bac"
"k and forth. 2) Use folderfilter settings on a reposit"
"ory to prevent some folders from being created on the "
"other side." %
msg = "INFINITE FOLDER CREATION DETECTED! "\
"Folder '%s' (repository '%s') would be created as " \
"folder '%s' (repository '%s'). The latter " \
"becomes '%s' in return, leading to infinite " \
"folder creation cycles.\n "\
"SOLUTION: 1) Do set your nametrans rules on both " \
"repositories so they lead to identical names if " \
"applied back and forth. " \
"2) Use folderfilter settings on a repository to " \
"prevent some folders from being created on the " \
"other side." % \
(local_folder.getname(), local_repo, remote_name,
remote_repo,
loop_name),
OfflineImapError.ERROR.REPO)
remote_repo, loop_name)
raise OfflineImapError(msg, OfflineImapError.ERROR.REPO)
# End sanity check, actually create the folder.
try:
remote_repo.makefolder(remote_name)
# Need to refresh list.
self.forgetfolders()
except OfflineImapError as e:
self.ui.error(e, exc_info()[2], "Creating folder %s on "
"repository %s" % (remote_name, remote_repo))
except OfflineImapError as exc:
msg = "Creating folder %s on repository %s" % \
(remote_name, remote_repo)
self.ui.error(exc, exc_info()[2], msg)
raise
status_repo.makefolder(local_name.replace(
local_repo.getsep(), status_repo.getsep()))
@ -284,14 +364,10 @@ class BaseRepository(CustomConfig.ConfigHelperMixin):
def startkeepalive(self):
"""The default implementation will do nothing."""
pass
def stopkeepalive(self):
"""Stop keep alive, but don't bother waiting
for the threads to terminate."""
pass
def getlocalroot(self):
""" Local root folder for storing messages.
Will not be set for remote repositories."""

View File

@ -1,20 +1,22 @@
# Gmail IMAP repository support
# Copyright (C) 2008-2016 Riccardo Murri <riccardo.murri@gmail.com> &
# contributors
#
# 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
"""
Gmail IMAP repository support
Copyright (C) 2008-2016 Riccardo Murri <riccardo.murri@gmail.com> &
contributors
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 offlineimap.repository.IMAP import IMAPRepository
from offlineimap import folder, OfflineImapError
@ -38,7 +40,7 @@ class GmailRepository(IMAPRepository):
We first check the usual IMAP settings, and then fall back to
imap.gmail.com if nothing is specified."""
try:
return super(GmailRepository, self).gethost()
return super().gethost()
except OfflineImapError:
# Nothing was configured, cache and return hardcoded
# one. See the parent class (IMAPRepository) for how this
@ -53,12 +55,13 @@ class GmailRepository(IMAPRepository):
https://accounts.google.com/o/oauth2/token if nothing is
specified."""
url = super(GmailRepository, self).getoauth2_request_url()
url = super().getoauth2_request_url()
if url is None:
# Nothing was configured, cache and return hardcoded one.
self.setoauth2_request_url("https://accounts.google.com/o/oauth2/token")
else:
# Nothing was configured, use a hardcoded one.
url = "https://accounts.google.com/o/oauth2/token"
self.setoauth2_request_url(url)
return self.oauth2_request_url
def getport(self):
@ -67,13 +70,13 @@ class GmailRepository(IMAPRepository):
This Gmail implementation first checks for the usual IMAP settings
and falls back to 993 if nothing is specified."""
port = super(GmailRepository, self).getport()
port = super().getport()
if port is None:
return 993
else:
if port is not None:
return port
return 993
def getssl(self):
ssl = self.getconfboolean('ssl', None)
@ -82,7 +85,7 @@ class GmailRepository(IMAPRepository):
# GMail. Maybe this should look more similar to gethost &
# we could just rely on the global "ssl = yes" default.
return True
else:
return ssl
def getpreauthtunnel(self):
@ -96,12 +99,16 @@ class GmailRepository(IMAPRepository):
return folder.Gmail.GmailFolder
def gettrashfolder(self):
# Where deleted mail should be moved
"""
Where deleted mail should be moved
"""
return self.getconf('trashfolder', '[Gmail]/Trash')
def getspamfolder(self):
# Depending on the IMAP settings (Settings -> Forwarding and
# POP/IMAP -> IMAP Access -> "When I mark a message in IMAP as
# deleted") GMail might also deletes messages upon EXPUNGE in
# the Spam folder.
"""
Depending on the IMAP settings (Settings -> Forwarding and
POP/IMAP -> IMAP Access -> "When I mark a message in IMAP as
deleted") GMail might also deletes messages upon EXPUNGE in
the Spam folder.
"""
return self.getconf('spamfolder', '[Gmail]/Spam')

View File

@ -1,31 +1,30 @@
# Maildir repository support
# Copyright (C) 2002-2015 John Goerzen & contributors
# <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
"""
Maildir repository support
Copyright (C) 2002-2015 John Goerzen & contributors
<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
"""
from offlineimap.repository.Maildir import MaildirRepository
from offlineimap.folder.GmailMaildir import GmailMaildirFolder
class GmailMaildirRepository(MaildirRepository):
def __init__(self, reposname, account):
"""Initialize a MaildirRepository object. Takes a path name
to the directory holding all the Maildir directories."""
super(GmailMaildirRepository, self).__init__(reposname, account)
"""
GMail Maildir Repository Class
"""
def getfoldertype(self):
return GmailMaildirFolder

View File

@ -1,21 +1,22 @@
""" IMAP repository support """
"""
IMAP repository support
# Copyright (C) 2002-2019 John Goerzen & contributors
#
# 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
Copyright (C) 2002-2019 John Goerzen & contributors
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
import netrc
import errno
@ -29,6 +30,9 @@ from offlineimap.utils.distro_utils import get_os_sslcertfile, \
class IMAPRepository(BaseRepository):
"""
IMAP Repository Class, children of BaseRepository
"""
def __init__(self, reposname, account):
self.idlefolders = None
BaseRepository.__init__(self, reposname, account)
@ -60,7 +64,7 @@ class IMAPRepository(BaseRepository):
self.kathread = ExitNotifyThread(target=self.imapserver.keepalive,
name="Keep alive " + self.getname(),
args=(keepalivetime, self.kaevent))
self.kathread.setDaemon(1)
self.kathread.setDaemon(True)
self.kathread.start()
def stopkeepalive(self):
@ -92,11 +96,25 @@ class IMAPRepository(BaseRepository):
return self.copy_ignore_eval(foldername)
def getholdconnectionopen(self):
"""
Value of holdconnectionopen or False if it is not set
Returns: Value of holdconnectionopen or False if it is not set
"""
if self.getidlefolders():
return True
return self.getconfboolean("holdconnectionopen", False)
def getkeepalive(self):
"""
This function returns the keepalive value. If it is not set, then
check if the getidlefolders is set. If getidlefolders is set, then
returns 29 * 60
Returns: keepalive value
"""
num = self.getconfint("keepalive", 0)
if num == 0 and self.getidlefolders():
return 29 * 60
@ -107,8 +125,8 @@ class IMAPRepository(BaseRepository):
This requires that self.imapserver has been initialized with an
acquireconnection() or it will still be `None`"""
assert self.imapserver.delim is not None, "'%s' " \
"repository called getsep() before the folder separator was " \
assert self.imapserver.delim is not None, \
"'%s' repository called getsep() before the folder separator was " \
"queried from the server" % self
return self.imapserver.delim
@ -124,12 +142,12 @@ class IMAPRepository(BaseRepository):
host = self.getconf('remotehosteval')
try:
host = self.localeval.eval(host)
except Exception as e:
except Exception as exc:
raise OfflineImapError(
"remotehosteval option for repository "
"'%s' failed:\n%s" % (self, e),
"'%s' failed:\n%s" % (self, exc),
OfflineImapError.ERROR.REPO,
exc_info()[2])
exc_info()[2]) from exc
if host:
self._host = host
return self._host
@ -141,7 +159,8 @@ class IMAPRepository(BaseRepository):
# No success.
raise OfflineImapError("No remote host for repository "
"'%s' specified." % self, OfflineImapError.ERROR.REPO)
"'%s' specified." % self,
OfflineImapError.ERROR.REPO)
def get_remote_identity(self):
"""Remote identity is used for certain SASL mechanisms
@ -154,6 +173,13 @@ class IMAPRepository(BaseRepository):
return identity
def get_auth_mechanisms(self):
"""
Get the AUTH mechanisms. We have (ranged from the strongest to weakest)
these methods: "GSSAPI", "XOAUTH2", "CRAM-MD5", "PLAIN", "LOGIN"
Returns: The supported AUTH Methods
"""
supported = ["GSSAPI", "XOAUTH2", "CRAM-MD5", "PLAIN", "LOGIN"]
# Mechanisms are ranged from the strongest to the
# weakest ones.
@ -164,16 +190,22 @@ class IMAPRepository(BaseRepository):
mechs = self.getconflist('auth_mechanisms', r',\s*',
default)
for m in mechs:
if m not in supported:
for mech in mechs:
if mech not in supported:
raise OfflineImapError("Repository %s: " % self +
"unknown authentication mechanism '%s'" % m,
OfflineImapError.ERROR.REPO)
"unknown authentication mechanism '%s'"
% mech, OfflineImapError.ERROR.REPO)
self.ui.debug('imap', "Using authentication mechanisms %s" % mechs)
return mechs
def getuser(self):
"""
Returns the remoteusereval or remoteuser or netrc user value.
Returns: Returns the remoteusereval or remoteuser or netrc user value.
"""
localeval = self.localeval
if self.config.has_option(self.getsection(), 'remoteusereval'):
@ -198,7 +230,8 @@ class IMAPRepository(BaseRepository):
return netrcentry[0]
try:
netrcentry = netrc.netrc('/etc/netrc').authenticators(self.gethost())
netrcentry = netrc.netrc('/etc/netrc')\
.authenticators(self.gethost())
except IOError as inst:
if inst.errno not in (errno.ENOENT, errno.EACCES):
raise
@ -207,6 +240,12 @@ class IMAPRepository(BaseRepository):
return netrcentry[0]
def getport(self):
"""
Returns remoteporteval value or None if not found.
Returns: Returns remoteporteval int value or None if not found.
"""
port = None
if self.config.has_option(self.getsection(), 'remoteporteval'):
@ -217,16 +256,40 @@ class IMAPRepository(BaseRepository):
return self.getconfint('remoteport', None)
def getipv6(self):
"""
Returns if IPv6 is set. If not set, then return None
Returns: Boolean flag if IPv6 is set.
"""
return self.getconfboolean('ipv6', None)
def getssl(self):
"""
Get the boolean SSL value. Default is True, used if not found.
Returns: Get the boolean SSL value. Default is True
"""
return self.getconfboolean('ssl', True)
def getsslclientcert(self):
"""
Return the SSL client cert (sslclientcert) or None if not found
Returns: SSL client key (sslclientcert) or None if not found
"""
xforms = [os.path.expanduser, os.path.expandvars, os.path.abspath]
return self.getconf_xform('sslclientcert', xforms, None)
def getsslclientkey(self):
"""
Return the SSL client key (sslclientkey) or None if not found
Returns: SSL client key (sslclientkey) or None if not found
"""
xforms = [os.path.expanduser, os.path.expandvars, os.path.abspath]
return self.getconf_xform('sslclientkey', xforms, None)
@ -254,7 +317,7 @@ class IMAPRepository(BaseRepository):
# Can't use above cacertfile because of abspath.
conf_sslacertfile = self.getconf('sslcacertfile', None)
if conf_sslacertfile == "OS-DEFAULT" or \
conf_sslacertfile == None or \
conf_sslacertfile is None or \
conf_sslacertfile == '':
cacertfile = get_os_sslcertfile()
if cacertfile is None:
@ -277,12 +340,30 @@ class IMAPRepository(BaseRepository):
return cacertfile
def gettlslevel(self):
"""
Returns the TLS level (tls_level). If not set, returns 'tls_compat'
Returns: TLS level (tls_level). If not set, returns 'tls_compat'
"""
return self.getconf('tls_level', 'tls_compat')
def getsslversion(self):
"""
Returns the SSL version. If not set, returns None.
Returns: SSL version. If not set, returns None.
"""
return self.getconf('ssl_version', None)
def getstarttls(self):
"""
Get the value of starttls. If not set, returns True
Returns: Value of starttls. If not set, returns True
"""
return self.getconfboolean('starttls', True)
def get_ssl_fingerprint(self):
@ -292,12 +373,29 @@ class IMAPRepository(BaseRepository):
comma-separated fingerprints in hex form."""
value = self.getconf('cert_fingerprint', "")
return [f.strip().lower().replace(":", "") for f in value.split(',') if f]
return [f.strip().lower().replace(":", "")
for f in value.split(',') if f]
def setoauth2_request_url(self, url):
"""
Set the OAUTH2 URL request.
Args:
url: OAUTH2 URL request
Returns: None
"""
self.oauth2_request_url = url
def getoauth2_request_url(self):
"""
Returns the OAUTH2 URL request from configuration (oauth2_request_url).
If it is not found, then returns None
Returns: OAUTH2 URL request (oauth2_request_url)
"""
if self.oauth2_request_url is not None: # Use cached value if possible.
return self.oauth2_request_url
@ -305,6 +403,14 @@ class IMAPRepository(BaseRepository):
return self.oauth2_request_url
def getoauth2_refresh_token(self):
"""
Get the OAUTH2 refresh token from the configuration
(oauth2_refresh_token)
If the access token is not found, then returns None.
Returns: OAUTH2 refresh token (oauth2_refresh_token)
"""
refresh_token = self.getconf('oauth2_refresh_token', None)
if refresh_token is None:
refresh_token = self.localeval.eval(
@ -315,6 +421,13 @@ class IMAPRepository(BaseRepository):
return refresh_token
def getoauth2_access_token(self):
"""
Get the OAUTH2 access token from the configuration (oauth2_access_token)
If the access token is not found, then returns None.
Returns: OAUTH2 access token (oauth2_access_token)
"""
access_token = self.getconf('oauth2_access_token', None)
if access_token is None:
access_token = self.localeval.eval(
@ -325,6 +438,13 @@ class IMAPRepository(BaseRepository):
return access_token
def getoauth2_client_id(self):
"""
Get the OAUTH2 client id (oauth2_client_id) from the configuration.
If not found, returns None
Returns: OAUTH2 client id (oauth2_client_id)
"""
client_id = self.getconf('oauth2_client_id', None)
if client_id is None:
client_id = self.localeval.eval(
@ -335,6 +455,13 @@ class IMAPRepository(BaseRepository):
return client_id
def getoauth2_client_secret(self):
"""
Get the OAUTH2 client secret (oauth2_client_secret) from the
configuration. If it is not found, then returns None.
Returns: OAUTH2 client secret
"""
client_secret = self.getconf('oauth2_client_secret', None)
if client_secret is None:
client_secret = self.localeval.eval(
@ -345,18 +472,51 @@ class IMAPRepository(BaseRepository):
return client_secret
def getpreauthtunnel(self):
"""
Get the value of preauthtunnel. If not found, then returns None.
Returns: Returns preauthtunnel value. If not found, returns None.
"""
return self.getconf('preauthtunnel', None)
def gettransporttunnel(self):
"""
Get the value of transporttunnel. If not found, then returns None.
Returns: Returns transporttunnel value. If not found, returns None.
"""
return self.getconf('transporttunnel', None)
def getreference(self):
"""
Get the reference value in the configuration. If the value is not found
then returns a double quote ("") as string.
Returns: The reference variable. If not set, then returns '""'
"""
return self.getconf('reference', '""')
def getdecodefoldernames(self):
"""
Get the boolean value of decodefoldernames configuration variable,
if the value is not found, returns False.
Returns: Boolean value of decodefoldernames, else False
"""
return self.getconfboolean('decodefoldernames', False)
def getidlefolders(self):
"""
Get the list of idlefolders from configuration. If the value is not
found, returns an empty list.
Returns: A list of idle folders
"""
if self.idlefolders is None:
self.idlefolders = self.localeval.eval(
self.getconf('idlefolders', '[]')
@ -364,11 +524,25 @@ class IMAPRepository(BaseRepository):
return self.idlefolders
def getmaxconnections(self):
"""
Get the maxconnections configuration value from configuration.
If the value is not set, returns 1 connection
Returns: Integer value of maxconnections configuration variable, else 1
"""
num1 = len(self.getidlefolders())
num2 = self.getconfint('maxconnections', 1)
return max(num1, num2)
def getexpunge(self):
"""
Get the expunge configuration value from configuration.
If the value is not set in the configuration, then returns True
Returns: Boolean value of expunge configuration variable
"""
return self.getconfboolean('expunge', True)
def getpassword(self):
@ -388,7 +562,21 @@ class IMAPRepository(BaseRepository):
# 1. Evaluate Repository 'remotepasseval'.
passwd = self.getconf('remotepasseval', None)
if passwd is not None:
return self.localeval.eval(passwd).encode('utf-8')
l_pass = self.localeval.eval(passwd)
# We need a str password
if isinstance(l_pass, bytes):
return l_pass.decode(encoding='utf-8')
elif isinstance(l_pass, str):
return l_pass
# If is not bytes or str, we have a problem
raise OfflineImapError("Could not get a right password format for"
" repository %s. Type found: %s. "
"Please, open a bug." %
(self.name, type(l_pass)),
OfflineImapError.ERROR.FOLDER)
# 2. Read password from Repository 'remotepass'.
password = self.getconf('remotepass', None)
if password is not None:
@ -398,9 +586,10 @@ class IMAPRepository(BaseRepository):
# 3. Read password from file specified in Repository 'remotepassfile'.
passfile = self.getconf('remotepassfile', None)
if passfile is not None:
fd = open(os.path.expanduser(passfile), 'r', 'utf-8')
password = fd.readline().strip()
fd.close()
file_desc = open(os.path.expanduser(passfile), 'r',
encoding='utf-8')
password = file_desc.readline().strip()
file_desc.close()
return password.encode('UTF-8')
# 4. Read password from ~/.netrc.
try:
@ -415,7 +604,8 @@ class IMAPRepository(BaseRepository):
return netrcentry[2]
# 5. Read password from /etc/netrc.
try:
netrcentry = netrc.netrc('/etc/netrc').authenticators(self.gethost())
netrcentry = netrc.netrc('/etc/netrc')\
.authenticators(self.gethost())
except IOError as inst:
if inst.errno not in (errno.ENOENT, errno.EACCES):
raise
@ -433,6 +623,13 @@ class IMAPRepository(BaseRepository):
return self.getfoldertype()(self.imapserver, foldername, self, decode)
def getfoldertype(self):
"""
This function returns the folder type, in this case
folder.IMAP.IMAPFolder
Returns: folder.IMAP.IMAPFolder
"""
return folder.IMAP.IMAPFolder
def connect(self):
@ -455,25 +652,26 @@ class IMAPRepository(BaseRepository):
listfunction = imapobj.lsub
try:
result, listresult = listfunction(directory=self.imapserver.reference, pattern='"*"')
result, listresult = \
listfunction(directory=self.imapserver.reference, pattern='"*"')
if result != 'OK':
raise OfflineImapError("Could not list the folders for"
" repository %s. Server responded: %s" %
(self.name, self, str(listresult)),
(self.name, str(listresult)),
OfflineImapError.ERROR.FOLDER)
finally:
self.imapserver.releaseconnection(imapobj)
for s in listresult:
if s is None or (isinstance(s, str) and s == ''):
for fldr in listresult:
if fldr is None or (isinstance(fldr, str) and fldr == ''):
# Bug in imaplib: empty strings in results from
# literals. TODO: still relevant?
continue
try:
flags, delim, name = imaputil.imapsplit(s)
flags, delim, name = imaputil.imapsplit(fldr)
except ValueError:
self.ui.error(
"could not correctly parse server response; got: %s" % s)
"could not correctly parse server response; got: %s" % fldr)
raise
flaglist = [x.lower() for x in imaputil.flagsplit(flags)]
if '\\noselect' in flaglist:
@ -486,12 +684,13 @@ class IMAPRepository(BaseRepository):
try:
for foldername in self.folderincludes:
try:
imapobj.select(imaputil.utf8_IMAP(foldername), readonly=True)
except OfflineImapError as e:
imapobj.select(imaputil.utf8_IMAP(foldername),
readonly=True)
except OfflineImapError as exc:
# couldn't select this folderinclude, so ignore folder.
if e.severity > OfflineImapError.ERROR.FOLDER:
if exc.severity > OfflineImapError.ERROR.FOLDER:
raise
self.ui.error(e, exc_info()[2],
self.ui.error(exc, exc_info()[2],
'Invalid folderinclude:')
continue
retval.append(self.getfoldertype()(
@ -504,17 +703,22 @@ class IMAPRepository(BaseRepository):
retval.sort(key=lambda x: str.lower(x.getvisiblename()))
else:
# do foldersort in a python3-compatible way
# http://bytes.com/topic/python/answers/844614-python-3-sorting-comparison-function
# http://bytes.com/topic/python/answers/ \
# 844614-python-3-sorting-comparison-function
def cmp2key(mycmp):
"""Converts a cmp= function into a key= function
We need to keep cmp functions for backward compatibility"""
class K:
"""
Class to compare getvisiblename() between two objects.
"""
def __init__(self, obj, *args):
self.obj = obj
def __cmp__(self, other):
return mycmp(self.obj.getvisiblename(), other.obj.getvisiblename())
return mycmp(self.obj.getvisiblename(),
other.obj.getvisiblename())
return K
@ -532,21 +736,26 @@ class IMAPRepository(BaseRepository):
try:
result = imapobj.delete(foldername)
if result[0] != 'OK':
raise OfflineImapError("Folder '%s'[%s] could not be deleted. "
"Server responded: %s" % (foldername, self, str(result)),
OfflineImapError.ERROR.FOLDER)
msg = "Folder '%s'[%s] could not be deleted. "\
"Server responded: %s" % (foldername, self, str(result))
raise OfflineImapError(msg, OfflineImapError.ERROR.FOLDER)
finally:
self.imapserver.releaseconnection(imapobj)
def makefolder(self, foldername):
"""Create a folder on the IMAP server
"""
Create a folder on the IMAP server
This will not update the list cached in :meth:`getfolders`. You
will need to invoke :meth:`forgetfolders` to force new caching
when you are done creating folders yourself.
:param foldername: Full path of the folder to be created."""
Args:
foldername: Full path of the folder to be created
Returns: None
"""
if foldername == '':
return
@ -558,15 +767,25 @@ class IMAPRepository(BaseRepository):
return
parts = foldername.split(self.getsep())
folder_paths = [self.getsep().join(parts[:n + 1]) for n in range(len(parts))]
folder_paths = [self.getsep().join(parts[:n + 1])
for n in range(len(parts))]
for folder_path in folder_paths:
try:
self.makefolder_single(folder_path)
except OfflineImapError as e:
if '[ALREADYEXISTS]' not in e.reason:
except OfflineImapError as exc:
if '[ALREADYEXISTS]' not in exc.reason:
raise
def makefolder_single(self, foldername):
"""
Create a IMAP folder.
Args:
foldername: Folder's name to create
Returns: None
"""
self.ui.makefolder(self, foldername)
if self.account.dryrun:
return
@ -577,13 +796,18 @@ class IMAPRepository(BaseRepository):
result = imapobj.create(foldername)
if result[0] != 'OK':
raise OfflineImapError("Folder '%s'[%s] could not be created. "
"Server responded: %s" % (foldername, self, str(result)),
OfflineImapError.ERROR.FOLDER)
msg = "Folder '%s'[%s] could not be created. "\
"Server responded: %s" % (foldername, self, str(result))
raise OfflineImapError(msg, OfflineImapError.ERROR.FOLDER)
finally:
self.imapserver.releaseconnection(imapobj)
class MappedIMAPRepository(IMAPRepository):
"""
This subclass of IMAPRepository includes only the method
getfoldertype modified that returns folder.UIDMaps.MappedIMAPFolder
instead of folder.IMAP.IMAPFolder
"""
def getfoldertype(self):
return folder.UIDMaps.MappedIMAPFolder

View File

@ -1,20 +1,21 @@
# Local status cache repository support
# Copyright (C) 2002-2017 John Goerzen & contributors
#
# 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
"""
Local status cache repository support
Copyright (C) 2002-2017 John Goerzen & contributors
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
from offlineimap.folder.LocalStatus import LocalStatusFolder
@ -24,6 +25,9 @@ from offlineimap.error import OfflineImapError
class LocalStatusRepository(BaseRepository):
"""
Local Status Repository Class, child of Base Repository Class
"""
def __init__(self, reposname, account):
BaseRepository.__init__(self, reposname, account)
@ -57,26 +61,44 @@ class LocalStatusRepository(BaseRepository):
return self.LocalStatusFolderClass(foldername, self) # Instantiate.
def setup_backend(self, backend):
"""
Setup the backend.
Args:
backend: backend to use
Returns: None
"""
if backend in list(self.backends.keys()):
self._backend = backend
self.root = self.backends[backend]['root']
self.LocalStatusFolderClass = self.backends[backend]['class']
def import_other_backend(self, folder):
for bk, dic in list(self.backends.items()):
"""
Import other backend
Args:
folder: folder
Returns: None
"""
for bkend, dic in list(self.backends.items()):
# Skip folder's own type.
if dic['class'] == type(folder):
continue
repobk = LocalStatusRepository(self.name, self.account)
repobk.setup_backend(bk) # Fake the backend.
repobk.setup_backend(bkend) # Fake the backend.
folderbk = dic['class'](folder.name, repobk)
# If backend contains data, import it to folder.
if not folderbk.isnewfolder():
self.ui._msg("Migrating LocalStatus cache from %s to %s "
"status folder for %s:%s" %
(bk, self._backend, self.name, folder.name))
(bkend, self._backend, self.name, folder.name))
folderbk.cachemessagelist()
folder.messagelist = folderbk.messagelist
@ -130,8 +152,6 @@ class LocalStatusRepository(BaseRepository):
the file names that we have available. TODO: need to store a
list of folder names somehow?"""
pass
def forgetfolders(self):
"""Forgets the cached list of folders, if any. Useful to run
after a sync run."""

View File

@ -1,20 +1,22 @@
# Maildir repository support
# Copyright (C) 2002-2015 John Goerzen & contributors
#
# 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
"""
Maildir repository support
Copyright (C) 2002-2015 John Goerzen & contributors
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
from offlineimap import folder
from offlineimap.ui import getglobalui
@ -23,6 +25,9 @@ from offlineimap.repository.Base import BaseRepository
class MaildirRepository(BaseRepository):
"""
Maildir Repository Class
"""
def __init__(self, reposname, account):
"""Initialize a MaildirRepository object. Takes a path name
to the directory holding all the Maildir directories."""
@ -32,7 +37,8 @@ class MaildirRepository(BaseRepository):
self.root = self.getlocalroot()
self.folders = None
self.ui = getglobalui()
self.debug("MaildirRepository initialized, sep is %s" % repr(self.getsep()))
self.debug("MaildirRepository initialized, sep is %s" %
repr(self.getsep()))
self.folder_atimes = []
# Create the top-level folder if it doesn't exist
@ -41,19 +47,19 @@ class MaildirRepository(BaseRepository):
# Create the keyword->char mapping
self.keyword2char = dict()
for c in 'abcdefghijklmnopqrstuvwxyz':
confkey = 'customflag_' + c
for char in 'abcdefghijklmnopqrstuvwxyz':
confkey = 'customflag_' + char
keyword = self.getconf(confkey, None)
if keyword is not None:
self.keyword2char[keyword] = c
self.keyword2char[keyword] = char
def _append_folder_atimes(self, foldername):
"""Store the atimes of a folder's new|cur in self.folder_atimes"""
p = os.path.join(self.root, foldername)
new = os.path.join(p, 'new')
cur = os.path.join(p, 'cur')
atimes = (p, os.path.getatime(new), os.path.getatime(cur))
path = os.path.join(self.root, foldername)
new = os.path.join(path, 'new')
cur = os.path.join(path, 'cur')
atimes = (path, os.path.getatime(new), os.path.getatime(cur))
self.folder_atimes.append(atimes)
def restore_atime(self):
@ -75,6 +81,15 @@ class MaildirRepository(BaseRepository):
return self.getconf_xform('localfolders', xforms)
def debug(self, msg):
"""
Debug function for the message. It calls the ui.debug function and
prepends the string 'maildir'.
Args:
msg: Message to send to the debug
Returns: None
"""
self.ui.debug('maildir', msg)
def getsep(self):
@ -107,24 +122,26 @@ class MaildirRepository(BaseRepository):
assert component not in ['new', 'cur', 'tmp'], \
"When using nested folders (/ as a Maildir separator), " \
"folder names may not contain 'new', 'cur', 'tmp'."
assert foldername.find('../') == -1, "Folder names may not contain ../"
assert not foldername.startswith('/'), "Folder names may not begin with /"
assert foldername.find('../') == -1, \
"Folder names may not contain ../"
assert not foldername.startswith('/'), \
"Folder names may not begin with /"
# If we're using hierarchical folders, it's possible that
# sub-folders may be created before higher-up ones.
self.debug("makefolder: calling makedirs '%s'" % full_path)
try:
os.makedirs(full_path, 0o700)
except OSError as e:
if e.errno == 17 and os.path.isdir(full_path):
except OSError as exc:
if exc.errno == 17 and os.path.isdir(full_path):
self.debug("makefolder: '%s' already a directory" % foldername)
else:
raise
for subdir in ['cur', 'new', 'tmp']:
try:
os.mkdir(os.path.join(full_path, subdir), 0o700)
except OSError as e:
if e.errno == 17 and os.path.isdir(full_path):
except OSError as exc:
if exc.errno == 17 and os.path.isdir(full_path):
self.debug("makefolder: '%s' already has subdir %s" %
(foldername, subdir))
else:
@ -142,11 +159,12 @@ class MaildirRepository(BaseRepository):
# getfolders() will scan and cache the values *if* necessary
folders = self.getfolders()
for f in folders:
if foldername == f.name:
return f
for foldr in folders:
if foldername == foldr.name:
return foldr
raise OfflineImapError("getfolder() asked for a nonexisting "
"folder '%s'." % foldername, OfflineImapError.ERROR.FOLDER)
"folder '%s'." % foldername,
OfflineImapError.ERROR.FOLDER)
def _getfolders_scandir(self, root, extension=None):
"""Recursively scan folder 'root'; return a list of MailDirFolder
@ -193,14 +211,15 @@ class MaildirRepository(BaseRepository):
self.debug(" This is maildir folder '%s'." % foldername)
if self.getconfboolean('restoreatime', False):
self._append_folder_atimes(foldername)
fd = self.getfoldertype()(self.root, foldername,
file_desc = self.getfoldertype()(self.root, foldername,
self.getsep(), self)
retval.append(fd)
retval.append(file_desc)
if self.getsep() == '/' and dirname != '':
# Recursively check sub-directories for folders too.
retval.extend(self._getfolders_scandir(root, foldername))
self.debug("_GETFOLDERS_SCANDIR RETURNING %s" % repr([x.getname() for x in retval]))
self.debug("_GETFOLDERS_SCANDIR RETURNING %s" %
repr([x.getname() for x in retval]))
return retval
def getfolders(self):
@ -209,6 +228,12 @@ class MaildirRepository(BaseRepository):
return self.folders
def getfoldertype(self):
"""
Returns a folder MaildirFolder type.
Returns: A MaildirFolder class
"""
return folder.Maildir.MaildirFolder
def forgetfolders(self):

View File

@ -1,19 +1,20 @@
# Copyright (C) 2002-2016 John Goerzen & contributors.
#
# 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
"""
Copyright (C) 2002-2016 John Goerzen & contributors.
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 sys import exc_info
from configparser import NoSectionError
from offlineimap.repository.IMAP import IMAPRepository, MappedIMAPRepository
@ -59,19 +60,19 @@ class Repository:
config = account.getconfig()
try:
repostype = config.get('Repository ' + name, 'type').strip()
except NoSectionError:
except NoSectionError as exc:
errstr = ("Could not find section '%s' in configuration. Required "
"for account '%s'." % ('Repository %s' % name, account))
raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO,
exc_info()[2])
exc_info()[2]) from exc
try:
repo = typemap[repostype]
except KeyError:
except KeyError as exc:
errstr = "'%s' repository not supported for '%s' repositories." % \
(repostype, reqtype)
raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO,
exc_info()[2])
exc_info()[2]) from exc
return repo(name, account)
@ -83,4 +84,3 @@ class Repository:
:param account: :class:`Account`
:param reqtype: 'remote', 'local', or 'status'
"""
pass

View File

@ -1,8 +1,9 @@
# Copyright (C) 2013-2014 Eygene A. Ryabinkin and contributors
#
# Collection of classes that implement const-like behaviour
# for various objects.
"""
Copyright (C) 2013-2014 Eygene A. Ryabinkin and contributors
Collection of classes that implement const-like behaviour
for various objects.
"""
import copy

View File

@ -1,7 +1,8 @@
# Copyright 2006-2018 Eygene A. Ryabinkin & contributors.
#
# Module that supports distribution-specific functions.
"""
Copyright 2006-2018 Eygene A. Ryabinkin & contributors.
Module that supports distribution-specific functions.
"""
import platform
import os
@ -68,12 +69,8 @@ def get_os_sslcertfile_searchpath():
Returned value of None means that there is no search path
at all.
"""
os_name = get_os_name()
location = None
if os_name in __DEF_OS_LOCATIONS:
location = __DEF_OS_LOCATIONS[os_name]
location = __DEF_OS_LOCATIONS.get(os_name, None)
return location
@ -92,9 +89,10 @@ def get_os_sslcertfile():
if location is None:
return None
for f in location:
assert isinstance(f, str)
if os.path.exists(f) and (os.path.isfile(f) or os.path.islink(f)):
return f
for l_file in location:
assert isinstance(l_file, str)
if os.path.exists(l_file) and (os.path.isfile(l_file) or
os.path.islink(l_file)):
return l_file
return None

View File

@ -1,6 +1,8 @@
# Copyright 2013 Eygene A. Ryabinkin
# Functions to perform stack tracing (for multithreaded programs
# as well as for single-threaded ones).
"""
Copyright 2013 Eygene A. Ryabinkin
Functions to perform stack tracing (for multithreaded programs
as well as for single-threaded ones).
"""
import sys
import threading
@ -10,14 +12,15 @@ import traceback
def dump(out):
""" Dumps current stack trace into I/O object 'out' """
id2name = {}
for th in threading.enumerate():
id2name[th.ident] = th.name
n = 0
for th_en in threading.enumerate():
id2name[th_en.ident] = th_en.name
count = 0
for i, stack in list(sys._current_frames().items()):
out.write("\n# Thread #%d (id=%d), %s\n" % (n, i, id2name[i]))
n = n + 1
for f, lno, name, line in traceback.extract_stack(stack):
out.write('File: "%s", line %d, in %s' % (f, lno, name))
out.write("\n# Thread #%d (id=%d), %s\n" % (count, i, id2name[i]))
count = count + 1
for file, lno, name, line in traceback.extract_stack(stack):
out.write('File: "%s", line %d, in %s' % (file, lno, name))
if line:
out.write(" %s" % (line.strip()))
out.write("\n")