aboutsummaryrefslogtreecommitdiff
path: root/bin/executable_mutt
diff options
context:
space:
mode:
Diffstat (limited to '')
-rwxr-xr-xbin/executable_mutt-fetchbug161
-rwxr-xr-xbin/executable_mutt_bgrun115
-rwxr-xr-xbin/executable_mutt_oauth2.py419
3 files changed, 695 insertions, 0 deletions
diff --git a/bin/executable_mutt-fetchbug b/bin/executable_mutt-fetchbug
new file mode 100755
index 0000000..93ffc58
--- /dev/null
+++ b/bin/executable_mutt-fetchbug
@@ -0,0 +1,161 @@
+#!/usr/bin/perl -w
+#
+# mutt-fetchbug, extensively based off of
+# mutt-notmuch - notmuch (of a) helper for Mutt
+#
+# Copyright: © 2011 Stefano Zacchiroli <zack@upsilon.cc>
+# License: GNU General Public License (GPL), version 3 or above
+#
+# Differences between mutt-notmuch and mutt-fetchbug are
+# Copyright: © 2012 Ryan Kavanagh <rak@debian.org>
+# License: GNU General Public License (GPL), version 3 or above
+#
+# See the bottom of this file for more documentation.
+# A manpage can be obtained by running "pod2man mutt-fetchbug > mutt-fetchbug.1"
+
+use strict;
+use warnings;
+
+use File::Path;
+use Getopt::Long;
+use Pod::Usage;
+
+# search($btsmbox, $query)
+# Fetch bugs matching $query with bts; store results in $btsmbox
+sub search($$) {
+ my ($btsmbox, $query) = @_;
+
+ system("bts --cache-mode=mbox cache $query"
+ . " && ln -fs ~/.cache/devscripts/bts/$query.mbox $btsmbox");
+}
+
+sub search_action($$@) {
+ my ($interactive, $btsmbox, @params) = @_;
+
+ if (! $interactive) {
+ fetch($btsmbox, join(' ', @params));
+ } else {
+ my $query = "";
+ my $done = 0;
+ while (! $done) {
+ print "bug number ('?' for man): ";
+ chomp($query = <STDIN>);
+ if ($query eq "?") {
+ system("man bts");
+ } elsif ($query eq "") {
+ $done = 1; # quit doing nothing
+ } else {
+ search($btsmbox, $query);
+ $done = 1;
+ }
+ }
+ }
+}
+
+sub die_usage() {
+ my %podflags = ( "verbose" => 1,
+ "exitval" => 2 );
+ pod2usage(%podflags);
+}
+
+sub main() {
+ my $btsmbox = "$ENV{HOME}/.cache/mutt_btsresults";
+ my $interactive = 0;
+ my $help_needed = 0;
+
+ my $getopt = GetOptions(
+ "h|help" => \$help_needed,
+ "o|output-mbox=s" => \$btsmbox,
+ "p|prompt" => \$interactive);
+ if (! $getopt || $#ARGV < 0) { die_usage() };
+ my ($action, @params) = ($ARGV[0], @ARGV[1..$#ARGV]);
+
+ if ($help_needed) {
+ die_usage();
+ } elsif ($action eq "search" && $#ARGV == 0 && ! $interactive) {
+ print STDERR "Error: no search term provided\n\n";
+ die_usage();
+ } elsif ($action eq "search") {
+ search_action($interactive, $btsmbox, @params);
+ } else {
+ die_usage();
+ }
+}
+
+main();
+
+__END__
+
+=head1 NAME
+
+mutt-fetchbug - 'bts show' frontend for Mutt
+
+=head1 SYNOPSIS
+
+=over
+
+=item B<mutt-fetchbug> [I<OPTION>]... search [I<SEARCH-TERM>]...
+
+=back
+
+=head1 DESCRIPTION
+
+mutt-fetchbug is a frontend to the 'bts show' command (Debian package:
+devscripts) designed to fetch bugs and place them in a predefined mbox. The
+search term should typically be a bug number.
+
+=head1 OPTIONS
+
+=over 4
+
+=item -o DIR
+
+=item --output-mbox DIR
+
+Store search results as (symlink) mbox MBOX. Beware: MBOX will be overwritten.
+(Default: F<~/.cache/mutt_btsresults/>)
+
+=item -p
+
+=item --prompt
+
+Instead of using command line search terms, prompt the user for them (only for
+"search").
+
+=item -h
+
+=item --help
+
+Show usage information and exit.
+
+=back
+
+=head1 INTEGRATION WITH MUTT
+
+mutt-fetchbug can be used to integrate 'bts show' with the Mutt mail user agent
+(unsurprisingly, given the name). To that end, you should define the following
+macros in your F<~/.muttrc> (replacing F<~/bin/mutt-fetchbug> for the actual
+location of mutt-fetchbug on your system):
+
+ macro index <F7> \
+ "<enter-command>unset wait_key<enter><shell-escape>~/bin/mutt-fetchbug --prompt search<enter><change-folder-readonly>~/.cache/mutt_btsresults<enter><enter-command>set wait_key<enter>" \
+ "fetch bug(s) (using bts show)"
+
+The macro (activated by <F7>) will prompt the user for a bug number and then
+jump to a temporary mbox showing the fetched bug.
+
+=head1 SEE ALSO
+
+mutt(1), bts(1)
+
+=head1 AUTHOR
+
+mutt-fetchbug is extensively based off of 'mutt-notmuch', which is
+Copyright: (C) 2011 Stefano Zacchiroli <zack@upsilon.cc>.
+
+All differences between mutt-fetchbug and mutt-notmuch are
+Copyright (C) 2012 Ryan Kavanagh <rak@debian.org>
+
+License: GNU General Public License (GPL), version 3 or higher
+
+=cut
diff --git a/bin/executable_mutt_bgrun b/bin/executable_mutt_bgrun
new file mode 100755
index 0000000..f833bab
--- /dev/null
+++ b/bin/executable_mutt_bgrun
@@ -0,0 +1,115 @@
+#!/bin/sh
+# @(#) mutt_bgrun $Revision: 1.4 $
+
+# mutt_bgrun - run an attachment viewer from mutt in the background
+# Copyright (C) 1999-2002 Gary A. Johnson
+#
+# 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+# SYNOPSIS
+# mutt_bgrun viewer [viewer options] file
+#
+# DESCRIPTION
+# Mutt invokes external attachment viewers by writing the
+# attachment to a temporary file, executing the pipeline specified
+# for that attachment type in the mailcap file, waiting for the
+# pipeline to terminate, writing nulls over the temporary file,
+# then deleting it. This causes problems when using graphical
+# viewers such as qvpview and acroread to view attachments.
+#
+# If qvpview, for example, is executed in the foreground, the mutt
+# user interface is hung until qvpview exits, so the user can't do
+# anything else with mutt until he or she finishes reading the
+# attachment and exits qvpview. This is especially annoying when
+# a message contains several MS Office attachments--one would like
+# to have them all open at once.
+#
+# If qvpview is executed in the background, it must be given
+# enough time to completely read the file before returning control
+# to mutt, since mutt will then obliterate the file. Qvpview is
+# so slow that this time can exceed 20 seconds, and the bound is
+# unknown. So this is again annoying.
+#
+# The solution provided here is to invoke the specified viewer
+# from this script after first copying mutt's temporary file to
+# another temporary file. This script can then quickly return
+# control to mutt while the viewer can take as much time as it
+# needs to read and render the attachment.
+#
+# EXAMPLE
+# To use qvpview to view MS Office attachments from mutt, add the
+# following lines to mutt's mailcap file.
+#
+# application/msword; mutt_bgrun qvpview %s
+# application/vnd.ms-excel; mutt_bgrun qvpview %s
+# application/vnd.ms-powerpoint; mutt_bgrun qvpview %s
+#
+# AUTHOR
+# Gary A. Johnson
+# <garyjohn@spk.agilent.com>
+#
+# ACKNOWLEDGEMENTS
+# My thanks to the people who have commented on this script and
+# offered solutions to shortcomings and bugs, especially Edmund
+# GRIMLEY EVANS <edmundo@rano.org> and Andreas Somogyi
+# <aso@somogyi.nu>.
+
+prog=${0##*/}
+
+# Check the arguments first.
+
+if [ "$#" -lt "2" ]
+then
+ echo "usage: $prog viewer [viewer options] file" >&2
+ exit 1
+fi
+
+# Separate the arguments. Assume the first is the viewer, the last is
+# the file, and all in between are options to the viewer.
+
+viewer="$1"
+shift
+
+while [ "$#" -gt "1" ]
+do
+ options="$options $1"
+ shift
+done
+
+file=$1
+
+# Create a temporary directory for our copy of the temporary file.
+#
+# This is more secure than creating a temporary file in an existing
+# directory.
+
+tmpdir=/tmp/$LOGNAME$$
+umask 077
+mkdir "$tmpdir" || exit 1
+tmpfile="$tmpdir/${file##*/}"
+
+# Copy mutt's temporary file to our temporary directory so that we can
+# let mutt overwrite and delete it when we exit.
+
+cp "$file" "$tmpfile"
+
+# Run the viewer in the background and delete the temporary files when done.
+
+(
+ "$viewer" $options "$tmpfile"
+ sleep 2
+ rm -f "$tmpfile"
+ rmdir "$tmpdir"
+) &
diff --git a/bin/executable_mutt_oauth2.py b/bin/executable_mutt_oauth2.py
new file mode 100755
index 0000000..f67364e
--- /dev/null
+++ b/bin/executable_mutt_oauth2.py
@@ -0,0 +1,419 @@
+#!/usr/bin/env python3
+#
+# Mutt OAuth2 token management script, version 2020-08-07
+# Written against python 3.7.3, not tried with earlier python versions.
+#
+# Copyright (C) 2020 Alexander Perlis
+#
+# 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 Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+'''Mutt OAuth2 token management'''
+
+import sys
+import json
+import argparse
+import urllib.parse
+import urllib.request
+import imaplib
+import poplib
+import smtplib
+import base64
+import secrets
+import hashlib
+import time
+from datetime import timedelta, datetime
+from pathlib import Path
+import socket
+import http.server
+import subprocess
+
+# The token file must be encrypted because it contains multi-use bearer tokens
+# whose usage does not require additional verification. Specify whichever
+# encryption and decryption pipes you prefer. They should read from standard
+# input and write to standard output. The example values here invoke GPG,
+# although won't work until an appropriate identity appears in the first line.
+ENCRYPTION_PIPE = ['cat']
+DECRYPTION_PIPE = ['cat']
+
+registrations = {
+ 'google': {
+ 'authorize_endpoint': 'https://accounts.google.com/o/oauth2/auth',
+ 'devicecode_endpoint': 'https://oauth2.googleapis.com/device/code',
+ 'token_endpoint': 'https://accounts.google.com/o/oauth2/token',
+ 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob',
+ 'imap_endpoint': 'imap.gmail.com',
+ 'pop_endpoint': 'pop.gmail.com',
+ 'smtp_endpoint': 'smtp.gmail.com',
+ 'sasl_method': 'OAUTHBEARER',
+ 'scope': 'https://mail.google.com/',
+ 'client_id': '',
+ 'client_secret': '',
+ },
+ 'microsoft': {
+ 'authorize_endpoint': 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
+ 'devicecode_endpoint': 'https://login.microsoftonline.com/common/oauth2/v2.0/devicecode',
+ 'token_endpoint': 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
+ 'redirect_uri': 'https://login.microsoftonline.com/common/oauth2/nativeclient',
+ 'tenant': 'common',
+ 'imap_endpoint': 'outlook.office365.com',
+ 'pop_endpoint': 'outlook.office365.com',
+ 'smtp_endpoint': 'smtp.office365.com',
+ 'sasl_method': 'XOAUTH2',
+ 'scope': ('offline_access https://outlook.office.com/IMAP.AccessAsUser.All '
+ 'https://outlook.office.com/POP.AccessAsUser.All '
+ 'https://outlook.office.com/SMTP.Send'),
+ 'client_id': '08162f7c-0fd2-4200-a84a-f25a4db0b584',
+ 'client_secret': 'TxRBilcHdC6WGBee]fs?QR:SJ8nI[g82',
+ },
+}
+
+ap = argparse.ArgumentParser(epilog='''
+This script obtains and prints a valid OAuth2 access token. State is maintained in an
+encrypted TOKENFILE. Run with "--verbose --authorize" to get started or whenever all
+tokens have expired, optionally with "--authflow" to override the default authorization
+flow. To truly start over from scratch, first delete TOKENFILE. Use "--verbose --test"
+to test the IMAP/POP/SMTP endpoints.
+''')
+ap.add_argument('-v', '--verbose', action='store_true', help='increase verbosity')
+ap.add_argument('-d', '--debug', action='store_true', help='enable debug output')
+ap.add_argument('tokenfile', help='persistent token storage')
+ap.add_argument('-a', '--authorize', action='store_true', help='manually authorize new tokens')
+ap.add_argument('--authflow', help='authcode | localhostauthcode | devicecode')
+ap.add_argument('-t', '--test', action='store_true', help='test IMAP/POP/SMTP endpoints')
+args = ap.parse_args()
+
+token = {}
+path = Path(args.tokenfile)
+if path.exists():
+ if 0o777 & path.stat().st_mode != 0o600:
+ sys.exit('Token file has unsafe mode. Suggest deleting and starting over.')
+ try:
+ sub = subprocess.run(DECRYPTION_PIPE, check=True, input=path.read_bytes(),
+ capture_output=True)
+ token = json.loads(sub.stdout)
+ except subprocess.CalledProcessError:
+ sys.exit('Difficulty decrypting token file. Is your decryption agent primed for '
+ 'non-interactive usage, or an appropriate environment variable such as '
+ 'GPG_TTY set to allow interactive agent usage from inside a pipe?')
+
+
+def writetokenfile():
+ '''Writes global token dictionary into token file.'''
+ if not path.exists():
+ path.touch(mode=0o600)
+ if 0o777 & path.stat().st_mode != 0o600:
+ sys.exit('Token file has unsafe mode. Suggest deleting and starting over.')
+ sub2 = subprocess.run(ENCRYPTION_PIPE, check=True, input=json.dumps(token).encode(),
+ capture_output=True)
+ path.write_bytes(sub2.stdout)
+
+
+if args.debug:
+ print('Obtained from token file:', json.dumps(token))
+if not token:
+ if not args.authorize:
+ sys.exit('You must run script with "--authorize" at least once.')
+ print('Available app and endpoint registrations:', *registrations)
+ token['registration'] = input('OAuth2 registration: ')
+ token['authflow'] = input('Preferred OAuth2 flow ("authcode" or "localhostauthcode" '
+ 'or "devicecode"): ')
+ token['email'] = input('Account e-mail address: ')
+ token['access_token'] = ''
+ token['access_token_expiration'] = ''
+ token['refresh_token'] = ''
+ writetokenfile()
+
+if token['registration'] not in registrations:
+ sys.exit(f'ERROR: Unknown registration "{token["registration"]}". Delete token file '
+ f'and start over.')
+registration = registrations[token['registration']]
+
+authflow = token['authflow']
+if args.authflow:
+ authflow = args.authflow
+
+baseparams = {'client_id': registration['client_id']}
+# Microsoft uses 'tenant' but Google does not
+if 'tenant' in registration:
+ baseparams['tenant'] = registration['tenant']
+
+
+def access_token_valid():
+ '''Returns True when stored access token exists and is still valid at this time.'''
+ token_exp = token['access_token_expiration']
+ return token_exp and datetime.now() < datetime.fromisoformat(token_exp)
+
+
+def update_tokens(r):
+ '''Takes a response dictionary, extracts tokens out of it, and updates token file.'''
+ token['access_token'] = r['access_token']
+ token['access_token_expiration'] = (datetime.now() +
+ timedelta(seconds=int(r['expires_in']))).isoformat()
+ if 'refresh_token' in r:
+ token['refresh_token'] = r['refresh_token']
+ writetokenfile()
+ if args.verbose:
+ print(f'NOTICE: Obtained new access token, expires {token["access_token_expiration"]}.')
+
+
+if args.authorize:
+ p = baseparams.copy()
+ p['scope'] = registration['scope']
+
+ if authflow in ('authcode', 'localhostauthcode'):
+ verifier = secrets.token_urlsafe(90)
+ challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest())[:-1]
+ redirect_uri = registration['redirect_uri']
+ listen_port = 0
+ if authflow == 'localhostauthcode':
+ # Find an available port to listen on
+ s = socket.socket()
+ s.bind(('127.0.0.1', 0))
+ listen_port = s.getsockname()[1]
+ s.close()
+ redirect_uri = 'http://localhost:'+str(listen_port)+'/'
+ # Probably should edit the port number into the actual redirect URL.
+
+ p.update({'login_hint': token['email'],
+ 'response_type': 'code',
+ 'redirect_uri': redirect_uri,
+ 'code_challenge': challenge,
+ 'code_challenge_method': 'S256'})
+ print(registration["authorize_endpoint"] + '?' +
+ urllib.parse.urlencode(p, quote_via=urllib.parse.quote))
+
+ authcode = ''
+ if authflow == 'authcode':
+ authcode = input('Visit displayed URL to retrieve authorization code. Enter '
+ 'code from server (might be in browser address bar): ')
+ else:
+ print('Visit displayed URL to authorize this application. Waiting...',
+ end='', flush=True)
+
+ class MyHandler(http.server.BaseHTTPRequestHandler):
+ '''Handles the browser query resulting from redirect to redirect_uri.'''
+
+ # pylint: disable=C0103
+ def do_HEAD(self):
+ '''Response to a HEAD requests.'''
+ self.send_response(200)
+ self.send_header('Content-type', 'text/html')
+ self.end_headers()
+
+ def do_GET(self):
+ '''For GET request, extract code parameter from URL.'''
+ # pylint: disable=W0603
+ global authcode
+ querystring = urllib.parse.urlparse(self.path).query
+ querydict = urllib.parse.parse_qs(querystring)
+ if 'code' in querydict:
+ authcode = querydict['code'][0]
+ self.do_HEAD()
+ self.wfile.write(b'<html><head><title>Authorizaton result</title></head>')
+ self.wfile.write(b'<body><p>Authorization redirect completed. You may '
+ b'close this window.</p></body></html>')
+ with http.server.HTTPServer(('127.0.0.1', listen_port), MyHandler) as httpd:
+ try:
+ httpd.handle_request()
+ except KeyboardInterrupt:
+ pass
+
+ if not authcode:
+ sys.exit('Did not obtain an authcode.')
+
+ for k in 'response_type', 'login_hint', 'code_challenge', 'code_challenge_method':
+ del p[k]
+ p.update({'grant_type': 'authorization_code',
+ 'code': authcode,
+ 'client_secret': registration['client_secret'],
+ 'code_verifier': verifier})
+ try:
+ response = urllib.request.urlopen(registration['token_endpoint'],
+ urllib.parse.urlencode(p).encode())
+ except urllib.error.HTTPError as err:
+ print(err.code, err.reason)
+ response = err
+ response = response.read()
+ if args.debug:
+ print(response)
+ response = json.loads(response)
+ if 'error' in response:
+ print(response['error'])
+ if 'error_description' in response:
+ print(response['error_description'])
+ sys.exit(1)
+
+ elif authflow == 'devicecode':
+ try:
+ response = urllib.request.urlopen(registration['devicecode_endpoint'],
+ urllib.parse.urlencode(p).encode())
+ except urllib.error.HTTPError as err:
+ print(err.code, err.reason)
+ response = err
+ response = response.read()
+ if args.debug:
+ print(response)
+ response = json.loads(response)
+ if 'error' in response:
+ print(response['error'])
+ if 'error_description' in response:
+ print(response['error_description'])
+ sys.exit(1)
+ print(response['message'])
+ del p['scope']
+ p.update({'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
+ 'client_secret': registration['client_secret'],
+ 'device_code': response['device_code']})
+ interval = int(response['interval'])
+ print('Polling...', end='', flush=True)
+ while True:
+ time.sleep(interval)
+ print('.', end='', flush=True)
+ try:
+ response = urllib.request.urlopen(registration['token_endpoint'],
+ urllib.parse.urlencode(p).encode())
+ except urllib.error.HTTPError as err:
+ # Not actually always an error, might just mean "keep trying..."
+ response = err
+ response = response.read()
+ if args.debug:
+ print(response)
+ response = json.loads(response)
+ if 'error' not in response:
+ break
+ if response['error'] == 'authorization_declined':
+ print(' user declined authorization.')
+ sys.exit(1)
+ if response['error'] == 'expired_token':
+ print(' too much time has elapsed.')
+ sys.exit(1)
+ if response['error'] != 'authorization_pending':
+ print(response['error'])
+ if 'error_description' in response:
+ print(response['error_description'])
+ sys.exit(1)
+ print()
+
+ else:
+ sys.exit(f'ERROR: Unknown OAuth2 flow "{token["authflow"]}. Delete token file and '
+ f'start over.')
+
+ update_tokens(response)
+
+
+if not access_token_valid():
+ if args.verbose:
+ print('NOTICE: Invalid or expired access token; using refresh token '
+ 'to obtain new access token.')
+ if not token['refresh_token']:
+ sys.exit('ERROR: No refresh token. Run script with "--authorize".')
+ p = baseparams.copy()
+ p.update({'client_secret': registration['client_secret'],
+ 'refresh_token': token['refresh_token'],
+ 'grant_type': 'refresh_token'})
+ try:
+ response = urllib.request.urlopen(registration['token_endpoint'],
+ urllib.parse.urlencode(p).encode())
+ except urllib.error.HTTPError as err:
+ print(err.code, err.reason)
+ response = err
+ response = response.read()
+ if args.debug:
+ print(response)
+ response = json.loads(response)
+ if 'error' in response:
+ print(response['error'])
+ if 'error_description' in response:
+ print(response['error_description'])
+ print('Perhaps refresh token invalid. Try running once with "--authorize"')
+ sys.exit(1)
+ update_tokens(response)
+
+
+if not access_token_valid():
+ sys.exit('ERROR: No valid access token. This should not be able to happen.')
+
+
+if args.verbose:
+ print('Access Token: ', end='')
+print(token['access_token'])
+
+
+def build_sasl_string(user, host, port, bearer_token):
+ '''Build appropriate SASL string, which depends on cloud server's supported SASL method.'''
+ if registration['sasl_method'] == 'OAUTHBEARER':
+ return f'n,a={user},\1host={host}\1port={port}\1auth=Bearer {bearer_token}\1\1'
+ if registration['sasl_method'] == 'XOAUTH2':
+ return f'user={user}\1auth=Bearer {bearer_token}\1\1'
+ sys.exit(f'Unknown SASL method {registration["sasl_method"]}.')
+
+
+if args.test:
+ errors = False
+
+ imap_conn = imaplib.IMAP4_SSL(registration['imap_endpoint'])
+ sasl_string = build_sasl_string(token['email'], registration['imap_endpoint'], 993,
+ token['access_token'])
+ if args.debug:
+ imap_conn.debug = 4
+ try:
+ imap_conn.authenticate(registration['sasl_method'], lambda _: sasl_string.encode())
+ # Microsoft has a bug wherein a mismatch between username and token can still report a
+ # successful login... (Try a consumer login with the token from a work/school account.)
+ # Fortunately subsequent commands fail with an error. Thus we follow AUTH with another
+ # IMAP command before reporting success.
+ imap_conn.list()
+ if args.verbose:
+ print('IMAP authentication succeeded')
+ except imaplib.IMAP4.error as e:
+ print('IMAP authentication FAILED (does your account allow IMAP?):', e)
+ errors = True
+
+ pop_conn = poplib.POP3_SSL(registration['pop_endpoint'])
+ sasl_string = build_sasl_string(token['email'], registration['pop_endpoint'], 995,
+ token['access_token'])
+ if args.debug:
+ pop_conn.set_debuglevel(2)
+ try:
+ # poplib doesn't have an auth command taking an authenticator object
+ # Microsoft requires a two-line SASL for POP
+ # pylint: disable=W0212
+ pop_conn._shortcmd('AUTH ' + registration['sasl_method'])
+ pop_conn._shortcmd(base64.standard_b64encode(sasl_string.encode()).decode())
+ if args.verbose:
+ print('POP authentication succeeded')
+ except poplib.error_proto as e:
+ print('POP authentication FAILED (does your account allow POP?):', e.args[0].decode())
+ errors = True
+
+ # SMTP_SSL would be simpler but Microsoft does not answer on port 465.
+ smtp_conn = smtplib.SMTP(registration['smtp_endpoint'], 587)
+ sasl_string = build_sasl_string(token['email'], registration['smtp_endpoint'], 587,
+ token['access_token'])
+ smtp_conn.ehlo('test')
+ smtp_conn.starttls()
+ smtp_conn.ehlo('test')
+ if args.debug:
+ smtp_conn.set_debuglevel(2)
+ try:
+ smtp_conn.auth(registration['sasl_method'], lambda _=None: sasl_string)
+ if args.verbose:
+ print('SMTP authentication succeeded')
+ except smtplib.SMTPAuthenticationError as e:
+ print('SMTP authentication FAILED:', e)
+ errors = True
+
+ if errors:
+ sys.exit(1)