123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- """ Base 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 re
- import os.path
- from sys import exc_info
- from offlineimap import CustomConfig
- from offlineimap.ui import getglobalui
- from offlineimap.error import OfflineImapError
- class BaseRepository(CustomConfig.ConfigHelperMixin):
- """
- Base Class for Repository
- """
- def __init__(self, reposname, account):
- self.ui = getglobalui()
- self.account = account
- self.config = account.getconfig()
- self.name = reposname
- 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)
- if not os.path.exists(self.uiddir):
- os.mkdir(self.uiddir, 0o700)
- self.mapdir = os.path.join(self.uiddir, 'UIDMapping')
- if not os.path.exists(self.mapdir):
- os.mkdir(self.mapdir, 0o700)
- # FIXME: self.uiddir variable name is lying about itself.
- self.uiddir = os.path.join(self.uiddir, 'FolderValidity')
- if not os.path.exists(self.uiddir):
- os.mkdir(self.uiddir, 0o700)
- self.nametrans = lambda foldername: foldername
- self.folderfilter = lambda foldername: 1
- self.folderincludes = []
- self.foldersort = None
- self.newmail_hook = None
- if self.config.has_option(self.getsection(), 'nametrans'):
- self.nametrans = self.localeval.eval(
- self.getconf('nametrans'), {'re': re})
- if self.config.has_option(self.getsection(), 'folderfilter'):
- self.folderfilter = self.localeval.eval(
- self.getconf('folderfilter'), {'re': re})
- if self.config.has_option(self.getsection(), 'folderincludes'):
- self.folderincludes = self.localeval.eval(
- self.getconf('folderincludes'), {'re': re})
- if self.config.has_option(self.getsection(), 'foldersort'):
- self.foldersort = self.localeval.eval(
- self.getconf('foldersort'), {'re': re})
- def restore_atime(self):
- """Sets folders' atime back to their values after a sync
- Controlled by the 'restoreatime' config parameter (default
- False), applies only to local Maildir mailboxes and does nothing
- on all other repository types."""
- def connect(self):
- """Establish a connection to the remote, if necessary. This exists
- so that IMAP connections can all be established up front, gathering
- passwords as needed. It was added in order to support the
- error recovery -- we need to connect first outside of the error
- trap in order to validate the password, and that's the point of
- this function."""
- def holdordropconnections(self):
- """
- Hold the drop connections functions.
- Returns: None
- """
- def dropconnections(self):
- """
- 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):
- return self.name
- @property
- def accountname(self):
- """Account name as string"""
- 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
- def getsection(self):
- return 'Repository ' + self.name
- # Interface from CustomConfig.ConfigHelperMixin
- def getconfig(self):
- return self.config
- @property
- def readonly(self):
- """Is the repository readonly?"""
- return self._readonly
- def getlocaleval(self):
- """
- Get the account local eval.
- Returns: LocalEval class for account.
- """
- return self.account.getlocaleval()
- def getfolders(self):
- """Returns a list of ALL folders on this server."""
- return []
- def forgetfolders(self):
- """Forgets the cached list of folders, if any. Useful to run
- after a sync run."""
- 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):
- """Should this folder be synced?"""
- return fname in self.folderincludes or self.folderfilter(fname)
- def should_create_folders(self):
- """Is folder creation enabled on this repository?
- 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)
- def makefolder(self, foldername):
- """
- 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):
- """Get the folder for this repo.
- WARNING: the signature changes whether it's remote or local:
- - remote types have the decode arg
- - local types don't have the decode arg
- """
- raise NotImplementedError
- def sync_folder_structure(self, local_repo, status_repo):
- """Sync the folders structure.
- It does NOT sync the contents of those folders. nametrans rules
- in both directions will be honored
- 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():
- # Quick exit if no folder creation is enabled on either side.
- return None
- remote_repo = self
- remote_hash, local_hash = {}, {}
- for folder in remote_repo.getfolders():
- remote_hash[folder.getname()] = folder
- for folder in local_repo.getfolders():
- local_hash[folder.getname()] = folder
- # Create new folders from remote to local.
- for remote_name, remote_folder in list(remote_hash.items()):
- # Don't create on local_repo, if it is readonly.
- if not local_repo.should_create_folders():
- break
- # 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()):
- try:
- local_repo.makefolder(local_name)
- # Need to refresh list.
- local_repo.forgetfolders()
- except OfflineImapError as exc:
- self.ui.error(exc, exc_info()[2],
- "Creating folder %s on repository %s" %
- (local_name, local_repo))
- raise
- status_repo.makefolder(local_name.replace(
- local_repo.getsep(), status_repo.getsep()))
- # Create new folders from local to remote.
- for local_name, local_folder in list(local_hash.items()):
- if not remote_repo.should_create_folders():
- # Don't create missing folder on readonly repo.
- break
- # 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()):
- # 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):
- 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
- # original local name?
- #
- # Apply remote nametrans to see if we end up with the same
- # name. We have:
- # - remote_name: local_name -> reverse nametrans + separator
- # We want local_name == loop_name from:
- # - remote_name -> remote (nametrans + separator) -> loop_name
- #
- # 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)
- loop_name = tmp_remotefolder.getvisiblename().replace(
- remote_repo.getsep(), local_repo.getsep())
- if local_name != loop_name:
- 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)
- 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 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()))
- # Find deleted folders.
- # TODO: We don't delete folders right now.
- return None
- def startkeepalive(self):
- """The default implementation will do nothing."""
- def stopkeepalive(self):
- """Stop keep alive, but don't bother waiting
- for the threads to terminate."""
- def getlocalroot(self):
- """ Local root folder for storing messages.
- Will not be set for remote repositories."""
- return None
|