Initial Commit
This commit is contained in:
81
mutt/colors
Normal file
81
mutt/colors
Normal file
@ -0,0 +1,81 @@
|
||||
# object can be one of:
|
||||
# * attachment
|
||||
# * body (match regexp in the body of messages)
|
||||
# * bold (hiliting bold patterns in the body of messages)
|
||||
# * error (error messages printed by Mutt)
|
||||
# * header (match regexp in the message header)
|
||||
# * hdrdefault (default color of the message header in the pager)
|
||||
# * index (match pattern in the message index)
|
||||
# * indicator (arrow or bar used to indicate the current item in a menu)
|
||||
# * markers (the '+' markers at the beginning of wrapped lines in the pager)
|
||||
# * message (informational messages)
|
||||
# * normal (normal (not quoted) text
|
||||
# * quoted (text matching $quote_regexp in the body of a message)
|
||||
# * quoted1, quotedN (higher levels of quoting)
|
||||
# * search (hiliting of words in the pager)
|
||||
# * signature
|
||||
# * status (mode lines used to display info about the mailbox or message)
|
||||
# * tilde (the '~' used to pad blank lines in the pager)
|
||||
# * tree (thread tree drawn in the message index and attachment menu)
|
||||
# * underline (hiliting underlined patterns in the body of messages)
|
||||
#
|
||||
#
|
||||
# foreground and background can be one of the following:
|
||||
# * white
|
||||
# * black
|
||||
# * green
|
||||
# * magenta
|
||||
# * blue
|
||||
# * cyan
|
||||
# * yellow
|
||||
# * red
|
||||
# * default
|
||||
|
||||
color status color010 color000
|
||||
color normal color015 color000
|
||||
color indicator color011 color000
|
||||
color tree color009 color000
|
||||
color attachment color009 color000
|
||||
color signature color005 color000
|
||||
color quoted color011 color000
|
||||
color message color015 color000
|
||||
color tilde color010 color000
|
||||
color markers color010 color000
|
||||
color header color011 color000 "(^Date):"
|
||||
color header color015 color000 "(^From):"
|
||||
color header color027 color000 "(^To):"
|
||||
color header color027 color000 "(^Cc):"
|
||||
color header color202 color000 "(^Subject):"
|
||||
mono header bold "^(Date|From|To|Cc|Subject):"
|
||||
|
||||
|
||||
|
||||
color index color013 color000 ~U
|
||||
color index color009 color000 ~F
|
||||
color index color027 color000 ~N
|
||||
color index color001 color000 ~D
|
||||
color index color000 color009 ~T
|
||||
|
||||
mono index bold ~U
|
||||
mono index bold ~F
|
||||
mono index bold ~N
|
||||
mono index bold ~D
|
||||
mono index bold ~T
|
||||
|
||||
color body color011 color000 "<[Ee]?[Bb]?[Gg]>"
|
||||
color body color011 color000 "<[Bb][Gg]>"
|
||||
color body color011 color000 "[;:]-*[)>(<|]"
|
||||
|
||||
|
||||
# # URLs
|
||||
color body color027 color000 "(http|ftp|news|telnet|finger)://[^ \”\t\r\n]*"
|
||||
color body color027 color000 "mailto:[-a-z_0-9.]+@[-a-z_0-9.]+"
|
||||
color body color027 color000 "[-a-z_0-9.%$]+@[-a-z_0-9.]+\\.[-a-z][-a-z]+"
|
||||
mono body bold "(http|ftp|news|telnet|finger)://[^ \”\t\r\n]*"
|
||||
mono body bold "mailto:[-a-z_0-9.]+@[-a-z_0-9.]+"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
26
mutt/comprofix
Normal file
26
mutt/comprofix
Normal file
@ -0,0 +1,26 @@
|
||||
set realname = "Matthew McKinnon"
|
||||
set from = "mmckinnon@comprofix.com"
|
||||
set imap_user = "mmckinnon@comprofix.com"
|
||||
set smtp_url="smtp://$imap_user@outlook.office365.com/"
|
||||
|
||||
#macro index d "s=Deleted Items\n" "move message to trash"
|
||||
#macro pager d "C=Deleted Items\n\n<exit><delete-message>" "move message to trash"
|
||||
|
||||
set imap_authenticators = "oauthbearer:xoauth2"
|
||||
set imap_oauth_refresh_command = "~/.mutt/mutt_oauth2.py ~/.mutt/oauth2"
|
||||
set smtp_authenticators = ${imap_authenticators}
|
||||
set smtp_oauth_refresh_command = ${imap_oauth_refresh_command}
|
||||
|
||||
|
||||
set folder = "imap://$imap_user@outlook.office365.com/"
|
||||
set spoolfile = "+Inbox"
|
||||
set record = "+Sent Items"
|
||||
set postponed = "+Drafts"
|
||||
set trash = "+Deleted Items"
|
||||
|
||||
|
||||
set move = no
|
||||
set imap_check_subscribed = yes
|
||||
#set imap_list_subscribed = yes
|
||||
mailboxes "+Inbox" "+Sent Items" "+Deleted Items" "+Drafts"
|
||||
|
16
mutt/lang
Normal file
16
mutt/lang
Normal file
@ -0,0 +1,16 @@
|
||||
# IMAP folders
|
||||
set my_drafts="Drafts"
|
||||
set my_drafts_noquote="Drafts"
|
||||
set my_sent="Sent<quote-char><space>Items"
|
||||
set my_sent_noquote="Sent Items"
|
||||
set my_trash="Deleted<quote-char><space>Items"
|
||||
set my_trash_noquote="Deleted Items"
|
||||
|
||||
# Quotation and date formats
|
||||
set attribution="The %d, %n wrote:"
|
||||
set date_format="!%m/%d/%Y %H:%M"
|
||||
|
||||
# Hours and dates
|
||||
set charset="utf-8"
|
||||
set assumed_charset="utf-8"
|
||||
set send_charset="utf-8:us-ascii"
|
19
mutt/mailcap
Normal file
19
mutt/mailcap
Normal file
@ -0,0 +1,19 @@
|
||||
#text/html; links %s; nametemplate=%s.html
|
||||
text/html; ( nohup qutebrowser %s > /dev/null 2>&1 & ); nametemplate=%s.html;
|
||||
text/html; lynx -dump %s; nametemplate=%s.html; copiousoutput;
|
||||
text/html; links -dump %s -codepage utf-8; nametemplate=%s.html; copiousoutput
|
||||
text/html; luakit '%s' &; test=test -n "$DISPLAY"; needsterminal;
|
||||
|
||||
text/plain; nohup less %s > /dev/null 2>&1 &
|
||||
|
||||
|
||||
application/pdf; { set -m \; /bin/mv -f -T %s %s.mv \; ( evince %s.mv >/dev/null 2>&1 \; /bin/rm -f %s.mv \; ) & } \; disown -a
|
||||
|
||||
#image/jpeg; feh %s
|
||||
#image/gif; feh %s
|
||||
#image/GIF; feh %s
|
||||
#image/JPG; feh %s
|
||||
#image/jpg; feh %s
|
||||
#image/png; feh %s
|
||||
image; ( nohup feh %s > /dev/null 2>&1 & )
|
||||
|
421
mutt/mutt_oauth2.py
Executable file
421
mutt/mutt_oauth2.py
Executable file
@ -0,0 +1,421 @@
|
||||
#!/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
|
||||
import readline
|
||||
|
||||
# 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 = ['gpg', '--encrypt', '--recipient', 'Comprofix Oauth2']
|
||||
DECRYPTION_PIPE = ['gpg', '--decrypt']
|
||||
|
||||
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})
|
||||
print('Exchanging the authorization code for an access 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'])
|
||||
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)
|
62
mutt/muttrc
Normal file
62
mutt/muttrc
Normal file
@ -0,0 +1,62 @@
|
||||
|
||||
source $HOME/.mutt/colors
|
||||
source ~/.mutt/comprofix
|
||||
|
||||
|
||||
macro index <f4> '<sync-mailbox><enter-command>source ~/.mutt/comprofix<enter><change-folder>!<enter>'
|
||||
|
||||
set move = no
|
||||
set confirmappend = no
|
||||
set mailcap_path = $HOME/.mutt/mailcap
|
||||
set sort = threads
|
||||
set sort_aux = reverse-last-date-received
|
||||
#set sort_aux = reverse-date-received
|
||||
set smart_wrap
|
||||
set signature=$HOME/.mutt/signature
|
||||
|
||||
#alternative_order text/plain test text/html
|
||||
|
||||
auto_view text/html
|
||||
auto_view image/png
|
||||
auto_view application/x-gunzip
|
||||
auto_view application/x-gzip
|
||||
|
||||
set sidebar_visible = yes
|
||||
set sidebar_width = 23
|
||||
#set sidebar_delim = '|'
|
||||
set sidebar_divider_char = '|'
|
||||
set sidebar_sort_method = 'path'
|
||||
|
||||
set sidebar_short_path = yes
|
||||
|
||||
color sidebar_new color027 color000
|
||||
|
||||
macro index b '<enter-command>toggle sidebar_visible<enter><refresh>'
|
||||
macro pager b '<enter-command>toggle sidebar_visible<enter><redraw-screen>'
|
||||
bind index B bounce-message
|
||||
|
||||
set index_format="%3C %Z %[!%d.%m %H:%M] %-35.35L (%5c) %s" # Bronski's favourite
|
||||
#set index_format="%4C %Z %[!%d %b %y %I:%M%p] %-35.35L (%5c) %s"
|
||||
#set index_format="%4C %Z %d %-15.15L (%-5c) %s"
|
||||
set status_chars = " *%A"
|
||||
set status_format = "───[ Folder: %f ]───[%r%m messages%?n? (%n new)?%?d? (%d to delete)?%?t? (%t tagged)? ]───%>─%?p?( %p postponed )?───"
|
||||
set pager_index_lines=12
|
||||
|
||||
bind index \CP sidebar-prev
|
||||
bind index \CN sidebar-next
|
||||
bind index \CO sidebar-open
|
||||
bind pager \CP sidebar-prev
|
||||
bind pager \CN sidebar-next
|
||||
bind pager \CO sidebar-open
|
||||
|
||||
|
||||
# Pretty it up
|
||||
# # # ----
|
||||
# # # default list of header fields to weed out when displaying mail
|
||||
# # # ignore them all and then unignore what you want to see
|
||||
ignore *
|
||||
unignore Date To From: Cc Subject X-Tts X-Label
|
||||
unignore x-mailing-list: posted-to:
|
||||
unignore x-mailer:
|
||||
hdr_order Date To From: Cc Subject X-Tts X-Label
|
||||
#
|
7
mutt/signature
Normal file
7
mutt/signature
Normal file
@ -0,0 +1,7 @@
|
||||
Matt McKinnon
|
||||
Comprofix Support
|
||||
Web: https://www.comprofix.com
|
||||
Email: support@comprofix.com
|
||||
--
|
||||
For more information, please reread.
|
||||
|
Reference in New Issue
Block a user