adding oauth script
This commit is contained in:
parent
6534c2b145
commit
2d11ded544
296
.config/tridactyl/tridactylrc-copied
Normal file
296
.config/tridactyl/tridactylrc-copied
Normal file
|
@ -0,0 +1,296 @@
|
|||
alias tabsort jsb browser.tabs.query({}).then(tabs => tabs.sort((t1, t2) => t1.url.localeCompare(t2.url)).forEach((tab, index) => browser.tabs.move(tab.id, {index})))
|
||||
alias tabuniq jsb browser.tabs.query({}).then(tabs => browser.tabs.remove(tabs.filter((tab, index) => tabs.slice(index + 1).find(t => t.url == tab.url)).map(tab => tab.id)))
|
||||
alias logall js let l=prompt() ; Object.keys(tri.config.get("logging")).forEach(k => tri.config.set("logging", k, l))
|
||||
alias playAllVideos js tri.native.run("mpv --really-quiet --ontop --keepaspect-window --profile=protocol.http " + Array.from(document.querySelectorAll("a, iframe, video")).reduce((s, e) => {let r=(/^https?:\/\/((www.)?youtu((\.be\/)|(be\.com\/((embed\/)|(watch\?v=))))[^ ]+)|(.+\.webm)$/);let l="";if(e.tagName=="IFRAME")l=e.src.match(r);else if(e.tagName=="A")l=e.href.match(r)||e.innerText.match(r);else if(e.tagName=="VIDEO")l=[e.currentSrc?e.currentSrc:e.src];console.log(l);return s+(l && l.length > 0 && s.indexOf(l[0])<0?"'"+l[0]+"' ":"")},""))
|
||||
alias gitclone jsb -p tri.native.run("git clone --depth=1 '" + JS_ARG + "' /home/me/prog/" + JS_ARG.split("/").slice(-1))
|
||||
alias rsssave jsb -p tri.native.run('cat >> ~/.config/newsboat/urls', JS_ARG + "\n")
|
||||
alias openGithubNotifications composite js Array.from(document.querySelectorAll("li.list-group-item > span:nth-child(1) > a:nth-child(2)")).map(e => e.href) | jsb -p JS_ARG.forEach(url => tri.excmds.tabopen(url))
|
||||
alias tn jsb let a = str.join("").split("/" + "/").slice(-1)[0]; tri.excmds.tabopen(a ? `https://intranet.adacore.com/crm/#/tn/${a}` : `https://intranet.adacore.com/crm/#/Tickets/myTNs`) //
|
||||
alias mktn js tri.native.run(`mkdir -v "$HOME/prog/${document.location.href.split("/").slice(-1)[0]}"`).then(r=>Array.from(document.querySelectorAll("#Files a")).forEach(a=>fetch(a.href).then(r=>r.text()).then(t=>tri.native.write(`${r.content.match(/'.*'\n/)[0].slice(1,-2)}/${a.href.split("/").slice(-1)[0]}`,t))))
|
||||
|
||||
autocmd DocLoad twitter.com urlmodify -t twitter.com nitter.net
|
||||
|
||||
bind O composite url2args | fillcmdline open
|
||||
bind gh followpage prev
|
||||
bind gl followpage next
|
||||
bind gc composite js window.location.href | gitclone
|
||||
bind ;gc hint -qW gitclone
|
||||
bind H tabprev
|
||||
bind L tabnext
|
||||
bind K forward
|
||||
bind J back
|
||||
bind y clipboard yankshort
|
||||
bind Y hint -p
|
||||
bind v composite hint -pipe a href | js -p tri.excmds.shellescape(JS_ARG) | exclaim_quiet mpv --ontop --keepaspect-window --profile=protocol.http
|
||||
bind V js tri.excmds.shellescape(document.location.href).then(url => tri.native.run(`mpv --ontop --keepaspect-window --profile=protocol.http '${url}'`))
|
||||
bind ;v composite hint -qpipe a href | js -p JS_ARG.map(h => `'${h}'`).join(" ") | ! mpv
|
||||
bind e hint -W js -p tri.native.run(`$HOME/bin/add-magnet '${JS_ARG}'`)
|
||||
bind u undo tab
|
||||
bind U undo window
|
||||
bind s fillcmdline saveas
|
||||
bind S saveas
|
||||
bind ;s hint -a
|
||||
bind ;S hint -s
|
||||
bind <A-v> playAllVideos
|
||||
" Bind <A-&..à> to `buffer 1..$` and <A-1..0> to `tabmove 1..$`
|
||||
jsb ["&", "é", '"', "'", "(", "-", "è", "_", "ç", "à"].forEach((l, i) => { i = (i == 9 ? 0 : (i + 1)); ["--mode=insert", "--mode=input", "--mode=normal"].forEach(mode => { tri.excmds.bind(mode, `<A-${l}>`, `buffer ${i}`) ; tri.excmds.bind(mode, `<A-${i}>`, `tabmove ${i}`) })})
|
||||
jsb ["--mode=insert", "--mode=input", "--mode=normal"].forEach(mode => { tri.excmds.bind(mode, `<A-H>`, `tabmove -1`) ; tri.excmds.bind(mode, `<A-L>`, `tabmove +1`) ; })
|
||||
bind --mode=normal <C-P> winopen -private
|
||||
bind --mode=ex <C-a> text.beginning_of_line
|
||||
bind --mode=insert <C-a> text.beginning_of_line
|
||||
bind --mode=input <C-a> text.beginning_of_line
|
||||
bind --mode=ex <C-e> text.end_of_line
|
||||
bind --mode=insert <C-e> text.end_of_line
|
||||
bind --mode=input <C-e> text.end_of_line
|
||||
bind --mode=ex <C-f> text.forward_word
|
||||
bind --mode=insert <C-f> text.forward_word
|
||||
bind --mode=input <C-f> text.forward_word
|
||||
bind --mode=ex <C-k> text.kill_line
|
||||
bind --mode=insert <C-k> text.kill_line
|
||||
bind --mode=input <C-k> text.kill_line
|
||||
bind --mode=ex <C-u> text.backward_kill_line
|
||||
bind --mode=insert <C-u> text.backward_kill_line
|
||||
bind --mode=input <C-u> text.backward_kill_line
|
||||
bind --mode=ex <C-V> composite getclip selection | text.insert_text
|
||||
bind --mode=insert <C-V> composite getclip selection | text.insert_text
|
||||
bind --mode=input <C-V> composite getclip selection | text.insert_text
|
||||
bind --mode=ex <C-w> text.backward_kill_word
|
||||
bind --mode=insert <C-w> text.backward_kill_word
|
||||
bind --mode=input <C-w> text.backward_kill_word
|
||||
|
||||
" Requires custom firefox build: https://github.com/glacambre/firefox-patches
|
||||
bind --mode=ex <C-n> ex.next_completion
|
||||
bind --mode=ex <C-p> ex.prev_completion
|
||||
bind --mode=ex <C-y> ex.insert_completion
|
||||
bind --mode=ex <C-e> ex.deselect_completion
|
||||
bind --mode=ex <Tab> ex.complete
|
||||
bind --mode=ex <C-g> composite text.beginning_of_line ; text.forward_word ; text.kill_word
|
||||
|
||||
" Disable <C-q>
|
||||
bind --mode=insert <C-q> js alert("<C-q> is for quitters.")
|
||||
bind --mode=input <C-q> js alert("<C-q> is for quitters.")
|
||||
bind --mode=normal <C-q> js alert("<C-q> is for quitters.")
|
||||
bind --mode=ex <C-q> js alert("<C-q> is for quitters.")
|
||||
bind --mode=hint <C-q> js alert("<C-q> is for quitters.")
|
||||
|
||||
unbind yy
|
||||
unbind ys
|
||||
unbind yc
|
||||
unbind ym
|
||||
unbind yt
|
||||
unbind --mode=ex <C-c>
|
||||
unbind --mode=ex <Space>
|
||||
unbind <SA-ArrowUp><SA-ArrowUp><SA-ArrowDown><SA-ArrowDown><SA-ArrowLeft><SA-ArrowRight><SA-ArrowLeft><SA-ArrowRight>ba
|
||||
|
||||
bindurl reddit.com <Space><Space> urlmodify -t www old
|
||||
bindurl https://github.com/.*/.*/blob f hint -c .blob-num,a
|
||||
bindurl https://github.com/notifications <Space><Space> openGithubNotifications
|
||||
bindurl youtu((\.be)|(be\.com)) f hint -J
|
||||
bindurl google(\.[a-zA-Z0-9]+){1,2}/search f hint -Jc #top_nav a, #search a, .card-section a, a.fl, #pnnext, #pnprev
|
||||
bindurl google(\.[a-zA-Z0-9]+){1,2}/search F hint -Jbc #top_nav a, #search a, .card-section a, a.fl, #pnnext, #pnprev
|
||||
bindurl google(\.[a-zA-Z0-9]+){1,2}/search gF hint -Jqbc #top_nav a, #search a, .card-section a, a.fl, #pnnext, #pnprev
|
||||
bindurl lkml.org/lkml gl js let lis = Array.from(document.querySelectorAll(".threadlist:nth-of-type(1)")[0].querySelectorAll("li")); document.location.href = lis[lis.findIndex(li => li.className.match("origin")) + 1].querySelector("a").href
|
||||
bindurl lkml.org/lkml gh js let lis = Array.from(document.querySelectorAll(".threadlist:nth-of-type(1)")[0].querySelectorAll("li")); document.location.href = lis[lis.findIndex(li => li.className.match("origin")) - 1].querySelector("a").href
|
||||
|
||||
set allowautofocus false
|
||||
set hintchars fdsqjklmrezauiopwxcvghtybn
|
||||
set searchengine g
|
||||
set tabopencontaineraware true
|
||||
set rsscmd rsssave %u
|
||||
set visualenterauto false
|
||||
set wordpattern [^\s\/]+
|
||||
|
||||
" Disable all searchurls
|
||||
jsb Object.keys(tri.config.get("searchurls")).reduce((prev, u) => prev.catch(()=>{}).then(_ => tri.excmds.setnull("searchurls." + u)), Promise.resolve())
|
||||
" Add our own
|
||||
set searchurls.amazon https://www.amazon.fr/s/ref=nb_sb_noss?field-keywords=%s
|
||||
set searchurls.bandcamp https://bandcamp.com/search?q=%s
|
||||
set searchurls.cnrtl http://www.cnrtl.fr/lexicographie/%s
|
||||
set searchurls.conj http://www.les-verbes.com/conjuguer.php?verbe=%s
|
||||
set searchurls.crates https://crates.io/search?q=%s
|
||||
set searchurls.ddg https://duckduckgo.com/html?q=%s
|
||||
set searchurls.deb https://packages.debian.org/search?keywords=%s&searchon=names&suite=all§ion=all
|
||||
set searchurls.fdroid https://search.f-droid.org/?q=%s
|
||||
set searchurls.g https://www.google.com/search?q=%s
|
||||
set searchurls.gh https://github.com/search?utf8=%E2%9C%93&q=%s&ref=simplesearch
|
||||
set searchurls.gi https://www.google.com/search?q=%s&tbm=isch
|
||||
set searchurls.gmaps https://www.google.com/maps/search/%s
|
||||
set searchurls.gw https://wiki.gentoo.org/index.php?title=Special%3ASearch&profile=default&search=%s&fulltext=Search
|
||||
set searchurls.imdb https://www.imdb.com/find?q=%s
|
||||
set searchurls.lqwant https://lite.qwant.com/?q=%s
|
||||
set searchurls.mdn https://developer.mozilla.org/en-US/search?q=%s&topic=api&topic=js
|
||||
set searchurls.monova https://monova.to/search?term=%s
|
||||
set searchurls.npm https://www.npmjs.com/search?q=%s
|
||||
set searchurls.osm https://www.openstreetmap.org/search?query=%s
|
||||
set searchurls.pydoc https://docs.python.org/3/search.html?q=%s
|
||||
set searchurls.qwant https://www.qwant.com/?q=%s
|
||||
set searchurls.ratp https://www.ratp.fr/itineraires?start=%s1&end=%s2&lieu_depart=&lieu_arrivee=&modes[rail]=rail&modes[metro]=metro&modes[bus]=bus&modes[tram]=tram&itinerary_profile=fastest&op=C%27est+parti
|
||||
set searchurls.r https://old.reddit.com/r/%s
|
||||
set searchurls.rustdoc https://doc.rust-lang.org/std/index.html?search=%s
|
||||
set searchurls.searxme https://searx.me/?q=%s&categories=general&language=en-US
|
||||
set searchurls.skyt https://www.skytorrents.to/?search=%s
|
||||
set searchurls.steam https://store.steampowered.com/search/?term=%s
|
||||
set searchurls.torrentz https://torrentz2.eu/search?f=%s
|
||||
set searchurls.tpb https://thepiratebay.org/s/?q=%s&=on&page=0&orderby=99
|
||||
set searchurls.tre http://www.wordreference.com/redirect/translation.aspx?w=%s&dict=enfr
|
||||
set searchurls.trf http://www.wordreference.com/redirect/translation.aspx?w=%s&dict=fren
|
||||
set searchurls.w https://en.wikipedia.org/w/index.php?search=%s&title=Special%3ASearch
|
||||
set searchurls.wfr https://fr.wikipedia.org/w/index.php?search=%s&title=Sp%E9cial%3ARecherche
|
||||
set searchurls.y https://www.youtube.com/results?search_query=%s
|
||||
set customthemes.custom html, body { -moz-font-feature-settings: "dlig" 0 !important; } span.TridactylHint { font-family: monospace !important; background: transparent !important; color: black !important; text-shadow: cyan -1px -1px 0px, cyan -1px 0px 0px, cyan -1px 1px 0px, cyan 1px -1px 0px, cyan 1px 0px 0px, cyan 1px 1px 0px, cyan 0px 1px 0px, cyan 0px -1px 0px !important; }
|
||||
set theme custom
|
||||
|
||||
seturl jsfiddle.net allowautofocus true
|
||||
|
||||
" Native messenger stuff
|
||||
guiset_quiet hoverlink right
|
||||
guiset_quiet tabs count
|
||||
setpref accessibility.typeaheadfind.autostart false
|
||||
setpref accessibility.typeaheadfind.flashBar 0
|
||||
setpref app.normandy.api_url ""
|
||||
setpref app.normandy.enabled false
|
||||
setpref app.normandy.first_run false
|
||||
setpref app.shield.optoutstudies.enabled false
|
||||
setpref app.update.enabled false
|
||||
setpref beacon.enabled false
|
||||
setpref beacon.enabled false
|
||||
setpref browser.autofocus false
|
||||
setpref browser.aboutHomeSnippets.updateUrl "data:,"
|
||||
setpref browser.display.use_document_fonts 0
|
||||
setpref browser.download.dir "/home/me/downloads"
|
||||
setpref browser.download.folderList 2
|
||||
setpref browser.download.manager.addToRecentDocs false
|
||||
setpref browser.download.useDownloadDir false
|
||||
setpref browser.eme.ui.enabled false
|
||||
setpref browser.feeds.handler.default "client"
|
||||
setpref browser.feeds.handlers.application "/home/me/bin/add_rss_feed"
|
||||
setpref browser.formfill.enable false
|
||||
setpref browser.helperApps.deleteTempFileOnExit true
|
||||
setpref browser.library.activity-stream.enabled false
|
||||
setpref browser.messaging-system.whatsNewPanel.enabled false
|
||||
setpref browser.newtab.preload false
|
||||
setpref browser.newtab.url "about:blank"
|
||||
setpref browser.newtabpage.enabled false
|
||||
setpref browser.newtabtabpage.enabled false
|
||||
setpref browser.newtabtabpage.enhanced false
|
||||
setpref browser.onboarding.enabled false
|
||||
setpref browser.pagethumbnails.capturing_disabled true
|
||||
setpref browser.ping-centre.telemetry false
|
||||
setpref browser.pocket.api ""
|
||||
setpref browser.pocket.oAuthConsumerKey ""
|
||||
setpref browser.pocket.site ""
|
||||
setpref browser.safebrowsing.appRepURL ""
|
||||
setpref browser.safebrowsing.blockedURIs.enabled false
|
||||
setpref browser.safebrowsing.downloads.remote.enabled false
|
||||
setpref browser.safebrowsing.downloads.remote.url ""
|
||||
setpref browser.safebrowsing.gethashURL ""
|
||||
setpref browser.safebrowsing.malware.enabled false
|
||||
setpref browser.safebrowsing.malware.reportURL ""
|
||||
setpref browser.safebrowsing.phishing.enabled false
|
||||
setpref browser.safebrowsing.provider.google.gethashURL ""
|
||||
setpref browser.safebrowsing.provider.google.lists ""
|
||||
setpref browser.safebrowsing.provider.google.reportMalwareMistakeURL ""
|
||||
setpref browser.safebrowsing.provider.google.reportPhishMistakeURL ""
|
||||
setpref browser.safebrowsing.provider.google.reportURL ""
|
||||
setpref browser.safebrowsing.provider.google.updateURL ""
|
||||
setpref browser.safebrowsing.provider.google4.dataSharing.enabled false
|
||||
setpref browser.safebrowsing.provider.google4.dataSharingURL ""
|
||||
setpref browser.safebrowsing.provider.google4.reportMalwareMistakeURL ""
|
||||
setpref browser.safebrowsing.provider.google4.reportPhishMistakeURL ""
|
||||
setpref browser.safebrowsing.provider.google4.reportURL ""
|
||||
setpref browser.safebrowsing.provider.mozilla.gethashURL ""
|
||||
setpref browser.safebrowsing.provider.mozilla.updateURL ""
|
||||
setpref browser.safebrowsing.reportPhishURL ""
|
||||
setpref browser.safebrowsing.reportURL ""
|
||||
setpref browser.safebrowsing.updateURL ""
|
||||
setpref browser.search.region "US"
|
||||
setpref browser.search.suggest.enabled false
|
||||
setpref browser.send_pings false
|
||||
setpref browser.send_pings.require_same_host true
|
||||
setpref browser.sessionstore.restore_on_demand false
|
||||
setpref browser.shell.checkDefaultBrowser false
|
||||
setpref browser.startup.homepage "about:blank"
|
||||
setpref browser.startup.homepage_override.mstone "ignore"
|
||||
setpref browser.startup.page 3
|
||||
setpref browser.tabs.closeWindowWithLastTab false
|
||||
setpref browser.tabs.remote.autostart.2 true
|
||||
setpref browser.uidensity 1
|
||||
setpref browser.urlbar.placeholderName ""
|
||||
setpref browser.urlbar.trimURLs false
|
||||
setpref datareporting.healthreport.uploadEnabled false
|
||||
setpref datareporting.policy.dataSubmissionEnabled false
|
||||
setpref devtools.gcli.hideIntro true
|
||||
setpref devtools.scratchpad.enabled true
|
||||
setpref devtools.scratchpad.wrapText true
|
||||
setpref devtools.webide.autoinstallADBHelper false
|
||||
setpref devtools.webide.enabled false
|
||||
setpref extensions.formautofill.addresses.enabled false
|
||||
setpref extensions.formautofill.available "off"
|
||||
setpref extensions.formautofill.creditCards.enabled false
|
||||
setpref extensions.formautofill.heuristics.enabled false
|
||||
setpref extensions.pocket.enabled false
|
||||
setpref extensions.screenshots.disabled true
|
||||
setpref extensions.screenshots.upload-disabled true
|
||||
setpref extensions.webcompat-reporter.enabled false
|
||||
setpref extensions.webextensions.restrictedDomains ""
|
||||
setpref font.blacklist.underline_offset ""
|
||||
setpref general.warnOnAboutConfig false
|
||||
setpref geo.enabled false
|
||||
setpref geo.wifi.uri ""
|
||||
setpref intl.accept_languages "en-US, en"
|
||||
setpref intl.locale.requested "en-US"
|
||||
setpref intl.regional_prefs.use_os_locales false
|
||||
setpref javascript.use_us_english_locale true
|
||||
setpref layout.css.font-loading-api.enabled false
|
||||
setpref media.autoplay.default 1
|
||||
setpref media.eme.enabled false
|
||||
setpref media.gmp-gmpopenh264.autoupdate false
|
||||
setpref media.gmp-gmpopenh264.enabled false
|
||||
setpref media.gmp-manager.updateEnabled false
|
||||
setpref media.gmp-manager.url "data:text/plain,"
|
||||
setpref media.gmp-manager.url.override "data:text/plain,"
|
||||
setpref media.gmp-provider.enabled false
|
||||
setpref media.gmp-widevinecdm.autoupdate false
|
||||
setpref media.gmp-widevinecdm.enabled false
|
||||
setpref media.gmp-widevinecdm.visible false
|
||||
setpref media.gmp.trial-create.enabled false
|
||||
" WebRTC. Might need to re-enable some day
|
||||
setpref media.peerconnection.enabled false
|
||||
setpref network.IDN_show_punycode true
|
||||
setpref network.allow-experiments false
|
||||
setpref network.http.referer.XOriginPolicy 1
|
||||
setpref network.http.referer.defaultPolicy 3
|
||||
setpref network.http.referer.defaultPolicy.pbmode 2
|
||||
setpref network.http.referer.spoofSource false
|
||||
setpref pdfjs.disabled true
|
||||
setpref permissions.default.geo 0
|
||||
setpref plugin.default.state 0
|
||||
setpref plugin.defaultXpi.state 0
|
||||
setpref plugin.sessionPermissionNow.intervalInMinutes 0
|
||||
setpref plugins.click_to_play true
|
||||
setpref privacy.firstparty.isolate true
|
||||
" Disabled until https://bugzilla.mozilla.org/show_bug.cgi?id=1450398 is fixed
|
||||
setpref privacy.resistFingerprinting false
|
||||
setpref privacy.resistFingerprinting.block_mozAddonManager true
|
||||
setpref privacy.userContext.enabled true
|
||||
setpref privacy.userContext.ui.enabled true
|
||||
setpref privacy.usercontext.about_newtab_segregation.enabled true
|
||||
setpref reader.parse-on-load.enabled false
|
||||
setpref security.dialog_enable_delay 500
|
||||
setpref security.insecure_field_warning.contextual.enabled true
|
||||
setpref signon.autofillForms false
|
||||
setpref signon.rememberSignons false
|
||||
setpref toolkit.cosmeticAnimations.enabled false
|
||||
setpref toolkit.telemetry.archive.enabled false
|
||||
setpref toolkit.telemetry.bhrPing.enabled false
|
||||
setpref toolkit.telemetry.cachedClientID ""
|
||||
setpref toolkit.telemetry.enabled false
|
||||
setpref toolkit.telemetry.firstShutdownPing.enabled false
|
||||
setpref toolkit.telemetry.hybridContent.enabled false
|
||||
setpref toolkit.telemetry.newProfilePing.enabled false
|
||||
setpref toolkit.telemetry.server "data:,"
|
||||
setpref toolkit.telemetry.shutdownPingSender.enabled false
|
||||
setpref toolkit.telemetry.unified false
|
||||
setpref toolkit.telemetry.updatePing.enabled false
|
||||
setpref ui.key.menuAccessKeyFocuses false
|
||||
setpref xpinstall.signatures.required false
|
421
scripts/mutt_oauth2.py
Executable file
421
scripts/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', 'chris']
|
||||
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': 'ca278be4-fe67-444c-9eae-d85dd486ae2c',
|
||||
'client_secret': 'An78Q~xvPz45DFH~lS~tZm3iVj-ApjUJNb~iKc0R',
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
Loading…
Reference in a new issue