diff options
Diffstat (limited to 'imap-dl')
-rwxr-xr-x | imap-dl | 107 |
1 files changed, 86 insertions, 21 deletions
@@ -3,6 +3,7 @@ # -*- coding: utf-8 -*- # Copyright (C) 2019-2020 Daniel Kahn Gillmor +# Copyright (C) 2020 Red Hat, Inc. # # 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 @@ -17,22 +18,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. -DESCRIPTION = '''A simple replacement for a minimalist use of getmail. +DESCRIPTION = '''Fetch messages from an IMAP inbox into a maildir -In particular, if you use getmail to reach an IMAP server as though it -were POP (retrieving from the server and optionally deleting), you can -point this script to the getmail config and it should do the same -thing. - -It tries to ensure that the configuration file is of the expected -type, and will terminate raising an exception, and it should not lose -messages. - -If there's any interest in supporting other similarly simple use cases -for getmail, patches are welcome. - -If you've never used getmail, you can make the simplest possible -config file like so: +Example config file: ---------- [retriever] @@ -46,6 +34,8 @@ path = /home/foo/Maildir [options] delete = True ---------- + +Run "man imap-dl" for more details. ''' import re @@ -61,13 +51,19 @@ import argparse import statistics import configparser -from typing import Dict, List, Union, Tuple +from typing import Dict, List, Optional, Tuple, Union try: import argcomplete #type: ignore except ImportError: argcomplete = None +try: + import gssapi # type: ignore +except ModuleNotFoundError: + gssapi = None + + class Splitter(object): def __init__(self, name:str, match:bytes): self._splitter = re.compile(match) @@ -91,6 +87,60 @@ summary_splitter = Splitter('summary', _summary_re) _fetch_re = rb'^(?P<id>[0-9]+) \(UID (?P<uid>[0-9]+) (FLAGS \([\\A-Za-z ]*\) )?BODY\[\] \{(?P<size>[0-9]+)\}$' fetch_splitter = Splitter('fetch', _fetch_re) +def auth_builtin(username:str, imap:imaplib.IMAP4_SSL, + conf:configparser.ConfigParser, server:str) -> None: + logging.info('Logging in as %s', username) + resp:Tuple[str, List[Union[bytes,Tuple[bytes,bytes]]]] + resp = imap.login(username, conf.get('retriever', 'password')) + if resp[0] != 'OK': + raise Exception(f'login failed with {resp} as user {username} on {server}') + +# imaplib auth methods need to be in the form of callables, and they all +# requre both additional parameters and storage beyond what the function +# interface provides. +class GSSAPI_handler(): + gss_vc:gssapi.SecurityContext + username:str + + def __init__(self, server:str, username:str) -> None: + name = gssapi.Name(f'imap@{server}', gssapi.NameType.hostbased_service) + self.gss_vc = gssapi.SecurityContext(usage="initiate", name=name) + self.username = username + + def __call__(self, token:Optional[bytes]) -> bytes: + if token == b"": + token = None + if not self.gss_vc.complete: + response = self.gss_vc.step(token) + return response if response else b"" # type: ignore + elif token is None: + return b"" + + response = self.gss_vc.unwrap(token) + + # Preserve the "length" of the message we received, and set the first + # byte to one. If username is provided, it's next. + reply:List[int] = [] + reply[0:4] = response.message[0:4] + reply[0] = 1 + if self.username: + reply[5:] = self.username.encode("utf-8") + + response = self.gss_vc.wrap(bytes(reply), response.encrypted) + return response.message if response.message else b"" # type: ignore + +def auth_gssapi(username:str, imap:imaplib.IMAP4_SSL, + conf:configparser.ConfigParser, server:str) -> None: + if not gssapi: + raise Exception('Kerberos requested, but python3-gssapi not found') + + logging.info(f'Logging in as {username} with GSSAPI') + + callback = GSSAPI_handler(server, username) + resp = imap.authenticate("GSSAPI", callback) + if resp[0] != 'OK': + raise Exception(f'GSSAPI login failed with {resp} as user {username} on {server}') + def scan_msgs(configfile:str, verbose:bool) -> None: conf = configparser.ConfigParser() conf.read_file(open(configfile, 'r')) @@ -127,16 +177,31 @@ def scan_msgs(configfile:str, verbose:bool) -> None: '(found "{on_size_mismatch_str}")') ctx = ssl.create_default_context(cafile=ca_certs) + ssl_ciphers = conf.get('retriever', 'ssl_ciphers', fallback=None) + if ssl_ciphers: + ctx.set_ciphers(ssl_ciphers) + server:str = conf.get('retriever', 'server') with imaplib.IMAP4_SSL(host=server, #type: ignore port=int(conf.get('retriever', 'port', fallback=993)), ssl_context=ctx) as imap: username:str = conf.get('retriever', 'username') - logging.info('Logging in as %s', username) - resp:Tuple[str, List[Union[bytes,Tuple[bytes,bytes]]]] - resp = imap.login(username, conf.get('retriever', 'password')) - if resp[0] != 'OK': - raise Exception(f'login failed with {resp} as user {username} on {server}') + authentication:str = conf.get('retriever', 'authentication', + fallback='basic') + # FIXME: have the default automatically choose an opinionated + # best authentication method. e.g., if the gssapi module is + # installed and the user has a reasonable identity in their + # local credential cache, choose kerberos, otherwise, choose + # "basic". + if authentication in ['kerberos', 'gssapi']: + auth_gssapi(username, imap, conf, server) + elif authentication == 'basic': + auth_builtin(username, imap, conf, server) + else: + # FIXME: implement other authentication mechanisms + raise Exception(f'retriever.authentication should be one of:\n' + '"basic" or "gssapi" (or "kerberos"). Got "{authentication}"') + if verbose: # only enable debugging after login to avoid leaking credentials in the log imap.debug = 4 logging.info("capabilities reported: %s", ', '.join(imap.capabilities)) |