diff --git a/Changelog.rst b/Changelog.rst index cdd871f..321bf87 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -8,6 +8,18 @@ ChangeLog OfflineIMAP v6.5.6.1 (YYYY-MM-DD) ================================= +* Expand environment variables in the following + configuration items: + - general.pythonfile; + - general.metadata; + - mbnames.filename; + - Repository.localfolders. + - Repository.sslcacertfile. + Make tilde and environment variable expansion in the following + configuration items: + - Repository.sslclientcert; + - Repository.sslclientkey. + * Updated bundled imaplib2 to 2.37: - add missing idle_lock in _handler() diff --git a/offlineimap.conf b/offlineimap.conf index c7cda28..94094a6 100644 --- a/offlineimap.conf +++ b/offlineimap.conf @@ -23,6 +23,12 @@ # NOTE2: This implies that any '%' needs to be encoded as '%%' +# NOTE3: Any variables that are subject to the environment variables +# ($NAME) and tilde (~username/~) expansions will receive tilde +# expansion first and only after this environment variables will be +# expanded in the resulting string. This behaviour is intentional +# as it coincides with typical shell expansion strategy. + ################################################## # General definitions ################################################## @@ -31,6 +37,8 @@ # This specifies where offlineimap is to store its metadata. # This directory will be created if it does not already exist. +# +# Tilde and environment variable expansions will be performed. #metadata = ~/.offlineimap @@ -89,6 +97,8 @@ accounts = Test # source file and call them from this config file. You can find # an example of this in the manual. # +# Tilde and environment variable expansions will be performed. +# # pythonfile = ~/.offlineimap.py # @@ -135,6 +145,9 @@ accounts = Test # - foldername: the name of the folder; # - localfolders: path to the local directory hosting all Maildir # folders for the account. +# +# Tilde and environment variable expansions will be performed +# for "filename" knob. enabled = no filename = ~/Mutt/muttrc.mailboxes @@ -375,9 +388,15 @@ remotehost = examplehost ssl = yes # SSL Client certificate (optional) +# +# Tilde and environment variable expansions will be performed. + # sslclientcert = /path/to/file.crt # SSL Client key (optional) +# +# Tilde and environment variable expansions will be performed. + # sslclientkey = /path/to/file.key # SSL CA Cert(s) to verify the server cert against (optional). @@ -385,6 +404,9 @@ ssl = yes # specified, the CA Cert(s) need to verify the Server cert AND # match the hostname (* wildcard allowed on the left hand side) # The certificate should be in PEM format. +# +# Tilde and environment variable expansions will be performed. + # sslcacertfile = /path/to/cacertfile.crt # If you connect via SSL/TLS (ssl=true) and you have no CA certificate diff --git a/offlineimap/CustomConfig.py b/offlineimap/CustomConfig.py index 9fef69a..1f4bcd0 100644 --- a/offlineimap/CustomConfig.py +++ b/offlineimap/CustomConfig.py @@ -94,14 +94,17 @@ class CustomConfigParser(SafeConfigParser): return default def getmetadatadir(self): - metadatadir = os.path.expanduser(self.getdefault("general", "metadata", "~/.offlineimap")) + xforms = [os.path.expanduser, os.path.expandvars] + d = self.getdefault("general", "metadata", "~/.offlineimap") + metadatadir = self.apply_xforms(d, xforms) if not os.path.exists(metadatadir): os.mkdir(metadatadir, 0o700) return metadatadir def getlocaleval(self): + xforms = [os.path.expanduser, os.path.expandvars] if self.has_option("general", "pythonfile"): - path = os.path.expanduser(self.get("general", "pythonfile")) + path = self.apply_xforms(self.get("general", "pythonfile"), xforms) else: path = None return LocalEval(path) @@ -132,6 +135,26 @@ class CustomConfigParser(SafeConfigParser): self.set(section, option, value) + def apply_xforms(self, string, transforms): + """ + Applies set of transformations to a string. + + Arguments: + - string: source string; if None, then no processing will + take place. + - transforms: iterable that returns transformation function + on each turn. + + Returns transformed string. + + """ + if string == None: + return None + for f in transforms: + string = f(string) + return string + + def CustomConfigDefault(): """ @@ -161,9 +184,10 @@ class ConfigHelperMixin: """ + def _confighelper_runner(self, option, default, defaultfunc, mainfunc, *args): """ - Return configuration or default value for option + Returns configuration or default value for option that contains in section identified by getsection(). Arguments: @@ -213,27 +237,96 @@ class ConfigHelperMixin: def getconf(self, option, default = CustomConfigDefault): + """ + Retrieves string from the configuration. + + Arguments: + - option: option name whose value is to be retrieved; + - default: default return value if no such option + exists. + + """ return self._confighelper_runner(option, default, self.getconfig().getdefault, self.getconfig().get) + + def getconf_xform(self, option, xforms, default = CustomConfigDefault): + """ + Retrieves string from the configuration transforming the result. + + Arguments: + - option: option name whose value is to be retrieved; + - xforms: iterable that returns transform functions + to be applied to the value of the option, + both retrieved and default one; + - default: default value for string if no such option + exists. + + """ + value = self.getconf(option, default) + return self.getconfig().apply_xforms(value, xforms) + + def getconfboolean(self, option, default = CustomConfigDefault): + """ + Retrieves boolean value from the configuration. + + Arguments: + - option: option name whose value is to be retrieved; + - default: default return value if no such option + exists. + + """ return self._confighelper_runner(option, default, self.getconfig().getdefaultboolean, self.getconfig().getboolean) + def getconfint(self, option, default = CustomConfigDefault): + """ + Retrieves integer value from the configuration. + + Arguments: + - option: option name whose value is to be retrieved; + - default: default return value if no such option + exists. + + """ return self._confighelper_runner(option, default, self.getconfig().getdefaultint, self.getconfig().getint) + def getconffloat(self, option, default = CustomConfigDefault): + """ + Retrieves floating-point value from the configuration. + + Arguments: + - option: option name whose value is to be retrieved; + - default: default return value if no such option + exists. + + """ return self._confighelper_runner(option, default, self.getconfig().getdefaultfloat, self.getconfig().getfloat) + def getconflist(self, option, separator_re, default = CustomConfigDefault): + """ + Retrieves strings from the configuration and splits it + into the list of strings. + + Arguments: + - option: option name whose value is to be retrieved; + - separator_re: regular expression for separator + to be used for split operation; + - default: default return value if no such option + exists. + + """ return self._confighelper_runner(option, default, self.getconfig().getdefaultlist, self.getconfig().getlist, separator_re) diff --git a/offlineimap/mbnames.py b/offlineimap/mbnames.py index 2d9ab5e..436a7e6 100644 --- a/offlineimap/mbnames.py +++ b/offlineimap/mbnames.py @@ -49,12 +49,14 @@ def write(): def __genmbnames(): """Takes a configparser object and a boxlist, which is a list of hashes containing 'accountname' and 'foldername' keys.""" + xforms = [os.path.expanduser, os.path.expandvars] mblock.acquire() try: localeval = config.getlocaleval() if not config.getdefaultboolean("mbnames", "enabled", 0): return - file = open(os.path.expanduser(config.get("mbnames", "filename")), "wt") + path = config.apply_xform(config.get("mbnames", "filename"), xforms) + file = open(path, "wt") file.write(localeval.eval(config.get("mbnames", "header"))) folderfilter = lambda accountname, foldername: 1 if config.has_option("mbnames", "folderfilter"): diff --git a/offlineimap/repository/IMAP.py b/offlineimap/repository/IMAP.py index c98419d..32cf3ac 100644 --- a/offlineimap/repository/IMAP.py +++ b/offlineimap/repository/IMAP.py @@ -194,18 +194,20 @@ class IMAPRepository(BaseRepository): return self.getconfboolean('ssl', 0) def getsslclientcert(self): - return self.getconf('sslclientcert', None) + xforms = [os.path.expanduser, os.path.expandvars, os.path.abspath] + return self.getconf_xform('sslclientcert', xforms, None) def getsslclientkey(self): - return self.getconf('sslclientkey', None) + xforms = [os.path.expanduser, os.path.expandvars, os.path.abspath] + return self.getconf_xform('sslclientkey', xforms, None) def getsslcacertfile(self): """Return the absolute path of the CA certfile to use, if any""" - cacertfile = self.getconf('sslcacertfile', get_os_sslcertfile()) + xforms = [os.path.expanduser, os.path.expandvars, os.path.abspath] + cacertfile = self.getconf_xform('sslcacertfile', xforms, + get_os_sslcertfile()) if cacertfile is None: return None - cacertfile = os.path.expanduser(cacertfile) - cacertfile = os.path.abspath(cacertfile) if not os.path.isfile(cacertfile): raise SyntaxWarning("CA certfile for repository '%s' could " "not be found. No such file: '%s'" \ diff --git a/offlineimap/repository/Maildir.py b/offlineimap/repository/Maildir.py index 0e5d65e..dae811d 100644 --- a/offlineimap/repository/Maildir.py +++ b/offlineimap/repository/Maildir.py @@ -61,7 +61,8 @@ class MaildirRepository(BaseRepository): os.utime(cur_dir, (cur_atime, os.path.getmtime(cur_dir))) def getlocalroot(self): - return os.path.expanduser(self.getconf('localfolders')) + xforms = [os.path.expanduser] + return self.getconf_xform('localfolders', xforms) def debug(self, msg): self.ui.debug('maildir', msg)