From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mp0 ([2001:41d0:2:4a6f::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by ms11 with LMTPS id iCkHGuMhtV/ESgAA0tVLHw (envelope-from ) for ; Wed, 18 Nov 2020 13:30:11 +0000 Received: from aspmx1.migadu.com ([2001:41d0:2:4a6f::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by mp0 with LMTPS id mPgDFuMhtV/GCwAA1q6Kng (envelope-from ) for ; Wed, 18 Nov 2020 13:30:11 +0000 Received: from mail.notmuchmail.org (nmbug.tethera.net [IPv6:2607:5300:201:3100::1657]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) server-signature RSA-PSS (2048 bits)) (No client certificate requested) by aspmx1.migadu.com (Postfix) with ESMTPS id 9B4669404E4 for ; Wed, 18 Nov 2020 13:30:08 +0000 (UTC) Received: from nmbug.tethera.net (localhost [127.0.0.1]) by mail.notmuchmail.org (Postfix) with ESMTP id 1D9EF28C49; Wed, 18 Nov 2020 08:29:58 -0500 (EST) Received: from knopi.disroot.org (knopi.disroot.org [178.21.23.139]) by mail.notmuchmail.org (Postfix) with ESMTPS id B83C22710B for ; Wed, 18 Nov 2020 08:29:55 -0500 (EST) Received: from localhost (localhost [127.0.0.1]) by disroot.org (Postfix) with ESMTP id E24A753466 for ; Wed, 18 Nov 2020 14:29:53 +0100 (CET) X-Virus-Scanned: Debian amavisd-new at disroot.org Received: from knopi.disroot.org ([127.0.0.1]) by localhost (disroot.org [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id Or4KEgA5-ki6 for ; Wed, 18 Nov 2020 14:29:50 +0100 (CET) From: "Jorge P. de Morais Neto" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=disroot.org; s=mail; t=1605706190; bh=D0wHyvbQB5wS8roPG1pzOAIQhH6vfBGgITJXx/lCzsw=; h=From:To:Subject:Date; b=I7+KcQJON7/mZEhLtZ+v2Yacc9VT9MlQSz4GzbQcVqMfxQFZ45pJlH7pfLHKadlHs z/lbsbbOW+PBwN4Q0jNgbp5IQHDNy9QQBzz2pIwSqeHN98OggaFWZnfSFb39bl6K7T FT5xxP0CsYt5on74f01L25J8jKqOSclTP89hCauuF6ZTDwpSbNuHNujC8zlmVCyjM/ 4xbUe5Kj7UXCSepk/SljFLG7FSUebB/aJPF6reHM05lJWEdPyO3d35QSVFU9mfGZxN wZWkyuEWg/fcN6crSvYZkFWCj6Eyh0/C4J7ztifO8HpIxg+6EMb68+mdMq+91Wyigu XUst1XR+UOtsQ== To: notmuch@notmuchmail.org Subject: Corrupted database (notmuch 0.31, Emacs 27.1.50, afew 3.0.1, OfflineIMAP, Python) Importance: high Mail-Followup-To: notmuch@notmuchmail.org Date: Wed, 18 Nov 2020 10:29:40 -0300 Message-ID: <878saykd2z.fsf@disroot.org> Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Message-ID-Hash: IK2MWRE4OIAMDYNLRQ5VQVUXVQ4LSPWA X-Message-ID-Hash: IK2MWRE4OIAMDYNLRQ5VQVUXVQ4LSPWA X-MailFrom: jorge+list@disroot.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation; header-match-notmuch.notmuchmail.org-0; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; suspicious-header X-Mailman-Version: 3.2.1 Precedence: list List-Id: "Use and development of the notmuch mail system." List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-Scanner: ns3122888.ip-94-23-21.eu X-Spam-Score: 6.00 Authentication-Results: aspmx1.migadu.com; dkim=fail (body hash did not verify) header.d=disroot.org header.s=mail header.b=I7+KcQJO; dmarc=fail reason="SPF not aligned (relaxed)" header.from=disroot.org (policy=quarantine); spf=pass (aspmx1.migadu.com: domain of notmuch-bounces@notmuchmail.org designates 2607:5300:201:3100::1657 as permitted sender) smtp.mailfrom=notmuch-bounces@notmuchmail.org X-Spam: Yes X-TUID: 3E+tQITz9fkO --=-=-= Content-Type: text/plain Hi. I use Notmuch 0.31.2 on Emacs 27.1.50 (manually compiled on 2020-11-02) with matching version-pinned MELPA Stable Notmuch package on updated Debian buster. I have enabled buster-proposed-updates, buster-updates and buster-backports. I manually backport notmuch according to . I use OfflineIMAP 7.3.3 (Python 2 pip), afew 3.0.1 (pip3), Bogofilter 1.2.4 (buster) and a custom Python 3 script based on the ~notmuch~ module. This Monday (when still on Notmuch 0.31) I noticed that, when I tagged a message or thread, the fido-mode completion offered many weird candidate tags that shouldn't exist in the database. Also, on the Notmuch Hello screen the ~All tags~ section would error out. I then dumped the database (~notmuch dump~) and noticed many lines associating weird tags to weird message ids. In almost every case, both the weird tags and the weird Message-Id contained uncommon characters, often ASCII control characters. One of the weird lines was " -- id:8"---specifying a message with Messaged-ID "8" and no tags. Invoking ~notmuch show id:8~ yielded internal error---something like "message with document ID has no thread ID". I then upgraded Notmuch to 0.31.2 and compacted the database but the error persisted. I then manually cleaned up the database dump, deleted the ~/offlineimap/Jorge-Disroot/.notmuch/xapian/ directory, invoked ~notmuch new~, and ~notmuch restore~. I checked my backups from 2020-11-09 (not affected) and 2011-11-16. That latest backup was from before I /noticed/ the corruption, but sadly it was affected too. I then diffed the latest backup with the previous; and then the latest backup with the current dump. The diffs suggest that the error involved only the addition of invalid information; I suspect and hope that valid information was not lost. I attached my post-new Bash script and the Python 3 script it invokes. So you can see the weird lines I mentioned, I provide for download (URL below) the xz-compressed output of the command: diff -u notmuch_dump--manually_fixed notmuch_dump--corrupted > diff_notmuch_dump__manually_fixed--corrupted https://upload.disroot.org/r/vSGNbFrN#8OCTbQTDKsrpRNjBsFzkv6rimPqEFE/UhGm14MypY0o= I have also saved the binary corrupted database. If you want to see it, then tell me and I may upload it to Disroot's Lufi instance. It should probably be shown to as few people as possible for the sake of my privacy. Finally, my notmuch config includes the following directives (the other directives are probably irrelevant to you): [new] tags=new ignore= [search] exclude_tags=deleted;spam;trash [maildir] synchronize_flags=true Regards --=-=-= Content-Type: application/x-shellscript Content-Disposition: inline; filename=post-new Content-Transfer-Encoding: base64 Content-Description: Notmuch post-new hook IyEvYmluL2Jhc2gKCnRpbWUgc2ltcGxlbXVjaC5weSAtdiBmaWx0ZXIKCiMgTG9jYWwgVmFyaWFi bGVzOgojIGlzcGVsbC1sb2NhbC1kaWN0aW9uYXJ5OiAiZW5fVVMiCiMgRW5kOgo= --=-=-= Content-Type: text/x-python; charset=utf-8 Content-Disposition: inline; filename=simplemuch.py Content-Transfer-Encoding: quoted-printable Content-Description: My Python3 Notmuch script: general filtering, Bogofilter, message notification #!/usr/bin/env python3 """Mail filter (including anti-spam) and notifier for Notmuch. Track messages classified as spam (or ham) by Bogofilter via '.bf_spam' (resp. '.bf_ham' ) tags. Since afew removes the `new' tag, when notifying mail we track new messages with a temporary tag (option '--tmp' of `filter' subcommand) which we assume not to preexist in the database. These tags and that added by the user to spam messages can be customized via command-line options or, from Python, by modifying module-level constants or via function arguments. This script is potentially affected by environment variables, files and directories that affect afew, Bogofilter, Notmuch or (obviously) Python3, including: 1. `NOTMUCH_CONFIG'---location of Notmuch configuration file---and that file itself. 2. `BOGOFILTER_DIR'---location of Bogofilter's database directory---and that directory itself. 3. afew configuration. WISH: Accept customizable "new" flags (currently we assume "new"). """ # WISH: Finish documenting the exceptions possibly raised by each function import logging import sys import time from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, Namespa= ce from functools import partial from logging import handlers from subprocess import PIPE, STDOUT, Popen, run from typing import Any, Callable, Iterable, Optional, Tuple, Union from notmuch import Database, Message # https://wiki.archlinux.org/index.php/Desktop_notifications#Python import gi # isort:skip gi.require_version('Notify', '0.7') # pylint: disable=3Dwrong-import-position from gi.repository import Notify # noqa: E402 isort:skip Tags =3D Union[str, Iterable[str]] LOG =3D logging.getLogger(__name__) FILTER_ACTIONS =3D {'spam', 'general', 'notify'} # Defaults for command-line options BF_HAM =3D '.bf_ham' BF_SPAM =3D '.bf_spam' USER_SPAM =3D 'spam' TMP =3D '_simplemuch_tmp' class SimplemuchError(Exception): """Base class for simplemuch exception classes""" class NotmuchDatabaseNeedsUpgradeError(SimplemuchError): """needs_upgrade() returned True.""" # WISH Capture more information, e.g. return code and command line class BogofilterError(SimplemuchError): """Error from Bogofilter""" # def teste_mypy(i: int) -> None: # return i + '' def alert(summary: str, body: str, *args: Any, fun: Callable[..., None] =3D LOG.warning) -> None: """Show desktop notification -- `summary', `body' -- and log. Logs with fun(body, *args). """ if fun in (LOG.exception, LOG.error): kwargs =3D {'icon': 'dialog-error'} elif fun in (LOG.warn, LOG.warning): kwargs =3D {'icon': 'dialog-warning'} else: kwargs =3D {} Notify.Notification.new(summary, body % args, **kwargs).show() fun(body, *args) def safe_open_db_rw() -> Database: """Open Notmuch database for reading and writing and return it. Before returning, check if the database needs upgrade; if so, raise NotmuchDatabaseNeedsUpgradeError. """ nm_db =3D Database(mode=3DDatabase.MODE.READ_WRITE) if nm_db.needs_upgrade(): raise NotmuchDatabaseNeedsUpgradeError( 'Notmuch database needs upgrade. Exiting without action.\n' 'WISH Implement correct database upgrading') return nm_db def update(nm_db: Database, args: Namespace, query: str, opr: str) -> Tuple[int, float]: """Call bogofilter on messages matching `query', change their tags. Call `bogofilter' with command-line option `opr' (plus -bl) and feed it (via stdin) the filenames of messages matching Notmuch query `query'. For each such message, apply the corresponding tag change (according to `args.bf_spam' and `args.bf_ham'). `opr' must be in set('SsNn') (see bogofilter(1) for the meaning). Return the number of messages operated on and the elapsed time. This function is potentially affected by environment variables, files and directories that affect Bogofilter or Notmuch. TODO Handle bogofilter errors """ start =3D time.time() assert opr in set('SsNn') tag_ =3D args.bf_spam if opr in 'sS' else args.bf_ham if opr in 'sn': def tag(msg: Message) -> None: msg.add_tag(tag_) else: def tag(msg: Message) -> None: msg.remove_tag(tag_) num =3D 0 with Popen(('bogofilter', '-bl' + opr), stdin=3DPIPE, text=3DTrue, bufsize=3D1) as bogo: assert bogo.stdin # Placate mypy for msg in nm_db.create_query(query).search_messages(): bogo.stdin.write(msg.get_filename() + '\n') tag(msg) num +=3D 1 if bogo.returncode: raise BogofilterError(f'Bogofilter returned {bogo.returncode}') return num, time.time() - start def train(args: Namespace) -> None: """Train Bogofilter on the Notmuch database. According to how the user classified the given message (spam or ham), update Simplemuch tags (`args.bf_spam' and `args.bf_ham') and Bogofilter's database. We assume the user classified a message as spam if it is tagged `args.user_spam'; and he classified it as ham if it has been read but not tagged `args.user_spam'. Therefore we assume that: 1. Messages tagged `args.user_spam' are in fact spam. 2. Spammy read messages are tagged `args.user_spam'. 3. Messages tagged `args.bf_spam' are also tagged `args.user_spam', unless they are false positives. A problematic scenario is when the user reads spam in webmail but forgets to tag it as spam in Notmuch. This function is potentially affected by environment variables, files and directories that affect Bogofilter or Notmuch. """ with safe_open_db_rw() as nm_db: def train_(query: str, opr: str, obj: str) -> None: assert opr in set('SsNn') opr_ =3D 'Register' if opr in 'sn' else 'Unregister' end =3D f'{opr_}ed %d {obj} in %.3gs' LOG.info('%s %s', opr_, obj) num, dur =3D update(nm_db, args, query, opr) LOG.info(end, num, dur) bf_spam, bf_ham, user_spam =3D args.bf_spam, args.bf_ham, args.user= _spam train_(f'is:{user_spam} NOT is:{bf_spam}', 's', 'spam') train_(f'is:{bf_spam} NOT is:{user_spam}', 'S', '(false) spam') train_(f'NOT (is:{user_spam} is:unread is:{bf_ham})', 'n', 'ham') train_(f'is:{user_spam} AND is:{bf_ham}', 'N', '(false) ham') def count(nm_db: Database, query: str, exclude: Tags =3D ()) -> int: """Return Xapian=E2=80=99s best guess as to how many messages match `qu= ery'. `exclude', if provided, must contain tags to exclude from the count by default. A given tag will not be excluded if it appears explicitly in `query'. May raise: - `NullPointerError' if the query creation failed (e.g. too little memory). - `NotInitializedError' if the underlying db was not initialized. This function is potentially affected by environment variables, files and directories that affect Notmuch. WISH Find out and document what "best guess" means; this wording is from the documentation of notmuch Python bindings. """ query_ =3D nm_db.create_query(query) if isinstance(exclude, str): query_.exclude_tag(exclude) else: for tag in exclude: query_.exclude_tag(tag) return query_.count_messages() def filter_spam(nm_db: Database, query: str, ham: Optional[str] =3D None, spam: Optional[str] =3D None) -> None: """Filter (Bogofilter) the messages matching Notmuch query `query'. If Bogofilter classifies a given message as Spam/Ham then tag it `spam'/`ham' (defaulting to `BF_SPAM'/`BF_HAM'). This function is potentially affected by environment variables, files and directories that affect Bogofilter or Notmuch. """ tag =3D dict(H=3Dham or BF_HAM, S=3Dspam or BF_SPAM) with Popen(('bogofilter', '-blT'), stdin=3DPIPE, stdout=3DPIPE, text=3D= True, bufsize=3D1) as bogo: assert bogo.stdin and bogo.stdout # Placate mypy for msg in nm_db.create_query(query).search_messages(): bogo.stdin.write(msg.get_filename() + '\n') code =3D bogo.stdout.readline().split()[-2] if code !=3D 'U': msg.add_tag(tag[code]) msg_id =3D msg.get_message_id() LOG.debug('Message %s marked %s', msg_id, tag[code]) def tag_search(nm_db: Database, query: str, add: Tags =3D (), remove: Tags =3D ()) -> None: """Add/remove tags from messages matching Notmuch `query'. `nm_db' must be open for reading and writing. `query' should be a Notmuch query on whose results we should act. Operate atomically on the set of messages matching `query'. May raise: - `XapianError'---see documentation of `begin_atomic()' and `end_atomic()' methods of `Database' - `NullPointerError' if notmuch query creation failed (e.g. too little memory) or `search_messages()' failed - `NotInitializedError' if the underlying db was not initialized - `NullPointerError' if a given tag is NULL - `TagTooLongError' if the length of a given tag exceeds notmuch.Message.NOTMUCH_TAG_MAX) - `ReadOnlyDatabaseError' if the database was opened in read-only mode - `NotInitializedError' if message has not been initialized This function is potentially affected by environment variables, files and directories that affect Notmuch. """ nm_db.begin_atomic() for msg in nm_db.create_query(query).search_messages(): if isinstance(add, str): msg.add_tag(add) else: for tag in add: msg.add_tag(tag) if isinstance(remove, str): msg.remove_tag(remove) else: for tag in remove: msg.remove_tag(tag) nm_db.end_atomic() def filter_notify(args: Namespace) -> None: """Filter mail (afew, Bogofilter and Notmuch) and notify. - `args.req' must be a container with elements of FILTER_ACTIONS we should act on (requested actions). - If \"args.req['spam']\" is True then `args.query' must be a string representing a Notmuch query (on whose results the spam filter will work) and `args.bf_ham', `args.bf_spam' must be the tags to add to messages that Bogofilter classifies as ham (resp. spam). - If `args.req' includes 'notify', we internally use a temporary tag---args.tmp---that we assume not to preexist in the Notmuch database. This function is potentially affected by environment variables, files and directories that affect afew, Bogofilter or Notmuch. TODO Document the required Notmuch saved queries. TODO Document the DKIM filtering. """ if args.req['general'] or args.req['notify'] or args.req['spam']: with safe_open_db_rw() as nm_db: if args.req['spam']: filter_spam(nm_db, args.query, args.bf_ham, args.bf_spam) if args.req['general'] or args.req['notify']: # Afew will remove 'new' tag_search(nm_db, 'is:new', args.tmp) tmp_count =3D count(nm_db, f'is:{args.tmp}') pipe =3D partial(run, stdout=3DPIPE, text=3DTrue) try: if args.req['general'] or args.req['notify']: exclude =3D pipe( ('notmuch', 'config', 'get', 'search.exclude_tags'), check=3DTrue).stdout.rstrip('\n').split('\n') if args.req['general']: afew =3D pipe(('afew', '-tnv'), check=3DTrue, stderr=3DSTDOUT) LOG.info('\n%s', afew.stdout) exclude_dkim =3D '(%s)' % ' OR '.join( (f'is:{tag}' for tag in exclude + ['/dkim-.*/'])) # problem =3D ('1584638185559.1b10c882-e1e1-4993-8f01-bdbcb3b4a= fe2@' # '302036m.grancursosonline.com.br') dkim_query =3D f'(is:{args.tmp} -{exclude_dkim})' afew =3D pipe(('afew', '-tv', '-eDKIMValidityFilter', dkim_quer= y), stderr=3DSTDOUT) if afew.returncode: alert('DKIM filter error', 'afew DKIMValidityFilter returned %d:\n%s', afew.returncode, afew.stdout) else: LOG.info('\n%s', afew.stdout) if args.req['general'] or args.req['notify']: with safe_open_db_rw() as nm_db: if args.req['notify']: p_count =3D partial(count, nm_db, exclude=3Dexclude) tmp_notify =3D f'is:{args.tmp} query:simplemuch_notify' notify =3D p_count(tmp_notify) if notify: unread =3D p_count("query:simplemuch_unread") inbox_unread =3D p_count("query:simplemuch_INBOX_un= read") flagged =3D p_count("query:simplemuch_flagged") body =3D (f'\ Inbox: {unread} unread ({inbox_unread} INBOX), {flagged} flagged\n' + '\n'.join(msg.get_header('Subject') for msg= in nm_db.create_query( tmp_notify).search_messages())) summary =3D f'{notify} new messages.' Notify.Notification.new( summary, body, 'mail-message-new').show() tag_search(nm_db, 'is:' + args.tmp, remove=3Dargs.tmp) tmp_count =3D 0 finally: if (args.req['notify'] or args.req['general']) and tmp_count: body_fmt =3D '%d messages left tagged %s' alert('Dirty messages', body_fmt, tmp_count, args.tmp) # Commented out since I don't know a simple way to obtain the location of t= he # Bogofilter directory. It may not be `(~/.bogofilter)': see Bogofilter # man page section `ENVIRONMENT'. Maybe the `-x' flag can help. # def clean(db, args): # """Remove Bogofilter tags from all messages and remove `(~/.bogofilte= r)'""" # if not shutil.rmtree.avoids_symlink_attacks: # print("Warning: this `shutil.rmtree' is susceptible to symlink at= tacks.") # while True: # reply =3D input(prompt=3D # f"""Remove Bogofilter database directory and, from all Notmuch email mess= ages, # {args.bf_spam} and {args.bf_ham} tags? [y/N] """).lower() # if 'no'.startswith(reply): # return False # if 'yes'.startswith(reply): # shutil.rmtree(os.path.expanduser('~/.bogofilter')) # tag_search(db, f'is:{args.bf_spam}', remove=3D'f{args.bf_spam= }') # tag_search(db, f'is:{args.bf_ham}', remove=3Df'{args.bf_ham}') # return True # print( # 'Please provide a valid answer: "yes", "no" or a prefix, ' # 'case-insensitive', file=3Dsys.stderr) def parse_command_line() -> Namespace: """Parse sys.argv into a Namespace object""" parser =3D ArgumentParser( description=3D__doc__, formatter_class=3DArgumentDefaultsHelpFormatter) parser.add_argument( '--version', action=3D'version', version=3D'Simplemuch alpha') parser.add_argument('-v', '--verbose', action=3D'store_true', help=3D'Output log messages also to stderr') parser.add_argument( '--bf_spam', default=3DBF_SPAM, metavar=3D'TAG', help=3D'Tag for bogofilter-flagged spam') parser.add_argument( '--bf_ham', default=3DBF_HAM, metavar=3D'TAG', help=3D'Tag for bogofilter-flagged ham') parser.add_argument( '--user_spam', default=3DUSER_SPAM, metavar=3D'TAG', help=3D'Tag for user-flagged spam') parser.add_argument( '--loglevel', default=3D'INFO', help=3D"""\ Severity threshold for logging; logging messages less severe are discarded. For the allowed values see https://docs.python.org/3/howto/logging.html""") subparsers =3D parser.add_subparsers( title=3D'Subcommands', required=3DTrue, description=3D'Specify exac= tly one') parser_filter =3D subparsers.add_parser( 'filter', help=3D"""Filter mail. By default (see `--skip'), filter= out spam, then do general mail filtering (with afew) and then, dependin= g on the new messages, notify.""") parser_filter.add_argument( '--skip', choices=3DFILTER_ACTIONS, nargs=3D'+', help=3D'Actions to= skip', default=3D()) # WISH: append a random suffix parser_filter.add_argument( '--tmp', metavar=3D'TEMPORARY_TAG', default=3DTMP, help=3D'Temporary tag for internal use; assumed by this script' ' not to preexist in the database') parser_filter.add_argument( 'query', nargs=3D'?', default=3D'is:new', help=3D'The Notmuch query whose result will be spam-filtered') parser_filter.set_defaults(func=3Dfilter_notify) parser_train =3D subparsers.add_parser( 'train', help=3D"""Train bogofilter. We assume the user classified a message as spam if it is tagged `args.user_spam'; and he classified a message as ham if it has been read but not tagged `args.user_spam'. Therefore we assume that: 1. Messages tagged `args.user_spam' are in fact spam. 2. Spammy read messages are tagged `args.user_spam'. 3. Messages tagged `args.bf_spam' are also tagged `args.user_spam', unless they are false positives. A problematic scenario is when the user reads spam in webmail but forgets to tag it spam in Notmuch.""") parser_train.set_defaults(func=3Dtrain) # parser_clean =3D subparsers.add_parser( # 'clean', # help=3D"Remove Bogofilter tags from all messages and remove " # "`(~/.bogofilter)'") # parser_clean.set_defaults(func=3Dclean) args =3D parser.parse_args() args.req =3D {a: args.func is filter_notify and a not in args.skip for a in FILTER_ACTIONS} # Requested actions return args def main() -> None: """Run as script: set up logging, parse sys.argv, execute.""" # WISH Maybe change the type of socket. See SysLogHandler documentation handler1 =3D handlers.SysLogHandler( address=3D'/dev/log', facility=3Dhandlers.SysLogHandler.LOG_MAIL) formatter =3D logging.Formatter( '%(module)s[%(process)d].%(funcName)s: %(levelname)s: %(message)s') handler1.setFormatter(formatter) LOG.addHandler(handler1) try: args =3D parse_command_line() # https://www.python.org/dev/peps/pep-0008/#programming-recommendat= ions except: # noqa: E722 LOG.exception( 'Exception occurred while parsing command line ("%s")', sys.arg= v) raise try: if args.verbose: handler2 =3D logging.StreamHandler() handler2.setFormatter(formatter) LOG.addHandler(handler2) level_num =3D getattr(logging, args.loglevel.upper(), None) if not isinstance(level_num, int): raise ValueError('Invalid log level: %s' % args.loglevel) LOG.setLevel(level_num) if args.req['notify']: # WISH Compute name from sys.argv[0], like argparse? Notify.init('Simplemuch') args.func(args) except: # noqa: E722 alert('Exception occurred', 'Command line: "%s"; parsed: %s', sys.a= rgv, args, fun=3DLOG.exception) raise if __name__ =3D=3D '__main__': main() # Local Variables: # ispell-local-dictionary: "en_US" # End: --=-=-= Content-Type: text/plain -- - - If an email of mine arrives at your spam box, please notify me. - Please adopt free/libre formats like PDF, ODF, Org, LaTeX, Opus, WebM and 7z. - Free/libre software for Replicant, LineageOS and Android: https://f-droid.org - [[https://www.gnu.org/philosophy/free-sw.html][What is free software?]] --=-=-= Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Content-Disposition: inline --=-=-=--