-
Notifications
You must be signed in to change notification settings - Fork 164
Contrib Hooks
https://github.com/pazz/alot/issues/563#issuecomment-13717496
def pre_envelope_send(ui, dbm):
ADDR = "[email protected]"
BCC_ADDR = "[email protected]"
from_fulladdr = ui.current_buffer.envelope.get_all("From")[0]
if ADDR in from_fulladdr:
ui.current_buffer.envelope.add("Bcc:", BCC_ADDR)
If it bothers you that a search buffer still shows unread messages after you have marked them read in another buffer and return to your search buffer, you may add the following hook:
def pre_buffer_focus(ui, dbm, buf):
if buf.modename == 'search':
buf.rebuild()
To maintain the position of focus in a search buffer after you open another buffer and return to the search buffer use the following hooks (based on pull request #633)
def pre_buffer_open(ui, dbm, buf):
current = ui.current_buffer
if isinstance(current, alot.buffers.SearchBuffer):
current.focused_thread = current.get_selected_thread() # remember focus
def post_buffer_focus(ui, dbm, buf, success):
if success and hasattr(buf, "focused_thread"): # if buffer has saved focus
tid = buf.focused_thread.get_thread_id()
for pos, tlw in enumerate(buf.threadlist.get_lines()):
if tlw.get_thread().get_thread_id() == tid:
buf.body.set_focus(pos)
break
https://github.com/pazz/alot/issues/395#issuecomment-13859844
from twisted.internet.defer import inlineCallbacks
import re
@inlineCallbacks
def pre_envelope_send(ui, dbm, _):
e = ui.current_buffer.envelope
if re.match('.*[Aa]ttach', e.body, re.DOTALL) and\
not e.attachments:
msg = 'no attachments. send anyway?'
if not (yield ui.choice(msg, select='yes')) == 'yes':
raise Exception()
Sometimes you want to change your From address based on where is the destination. I.e. you want to use work email address for company domain, project email address when addressing some mailing list and personal address otherwise. This will override every other setting:
import re
transitions = [
('.*work-domain\.com.*',
'Your Name <[email protected]>'),
('.*[email protected].*',
'Your Name <[email protected]>')
]
addr_trans = []
for addr, fr in transitions:
addr_trans.append((re.compile("(To|Cc): %s" % addr, re.MULTILINE),
"From: %s" % fr))
def pre_edit_translate(bodytext, ui, dbm):
for addr, new_from in addr_trans:
if addr.search(bodytext):
return re.sub('^From: .*$', new_from, bodytext, flags=re.MULTILINE)
return bodytext
Github automatically marks a notification as read if one views the corresponding
email in a html-aware mailclient. This is done by a getting a small invisible 1-pixel
beacon image. alot does not download this image per default. If you want to manually
mark some github notification as seen, you can define the hook below and triger
it using call
:
import re
import urllib2
def github_mark_read(ui):
msg = ui.current_buffer.get_selected_message()
msgtext = str(msg.get_email())
r = r"img src='(https://github.com/notifications/beacon/.*.gif)'"
beacons = re.findall(r, msgtext)
if beacons:
urllib2.urlopen(beacons[0])
ui.notify('removed from github notifications:\n %s' % beacons[0])
If you want to mark the focussed message (in thread mode) as read by hitting $
,
add the following to the binding section of the config:
[[bindings]]
[[thread]]
$ = call hooks.github_mark_read(ui)
https://github.com/pazz/alot/issues/656
Recent branches of alot (0.3.5-feature-mailto-666
)
contain helpers alot.helper.parse_mailto
and alot.helper.mailto_to_envelope
.
The hook below uses those to construct unsubscribe mails for mailing lists by
checking if (in thread mode) the currently highlighted message has a List-unsubscribe
header that contains a mailto-string.
With the hooks defines as below, use call hooks.unsubscribe()
to trigger the function.
This can of course also be bound to a keypress.
def unsubscribe():
"""
Unsubscribe from a mailing list.
This hook reads the 'List-Unsubscribe' header of a mail in thread mode,
constructs a unsubsribe-mail according to any mailto-url it finds
and opens the new mail in an envelope buffer.
"""
from alot.helper import mailto_to_envelope
from alot.buffers import EnvelopeBuffer
msg = ui.current_buffer.get_selected_message()
e = msg.get_email()
uheader = e['List-Unsubscribe']
dtheader = e.get('Delivered-To', None)
if uheader is not None:
M = re.search(r'<(mailto:\S*)>', uheader)
if M is not None:
env = mailto_to_envelope(M.group(1))
if dtheader is not None:
env['From'] = [dtheader]
ui.buffer_open(EnvelopeBuffer(ui, env))
else:
ui.notify('focussed mail contains no \'List-Unsubscribe\' header',
'error')
update 09/2017: This does not work any more. Apparently google has shut down the API that the goslate package uses. "Free lunch is over. Thanks for using." (https://pypi.python.org/pypi/goslate) A possible alternative is to use the google API in combination with a dev key directly. Contributions welcome!
[bindings]
[[thread]]
', t' = "call hooks.translate(ui)"
the hook code is below. It uses google translate via goslate: https://pypi.python.org/pypi/goslate .
def translate(ui, targetlang='en'):
# get msg content
msg = ui.current_buffer.get_selected_message()
msgtext = msg.accumulate_body()
# translate
import goslate
gs = goslate.Goslate()
tmsg = gs.translate(msgtext, targetlang)
# replace message widgets content
mt=ui.current_buffer.get_selected_messagetree()
mt.replace_bodytext(tmsg)
mt.refresh()
# refresh the thread buffer
ui.current_buffer.refresh()
To automatically decrypt PGP encrypted emails or remove the PGP header and signature from PGP signed emails, the following hook can be used:
from alot.settings.const import settings
import alot.crypto as crypto
from alot import errors
def text_quote(message):
# avoid importing a big module by using a simple heuristic to guess the
# right encoding
def decode(s, encodings=('ascii', 'utf8', 'latin1')):
for encoding in encodings:
try:
return s.decode(encoding)
except UnicodeDecodeError:
pass
return s.decode('ascii', 'ignore')
lines = message.splitlines()
if len(lines) == 0:
return ""
# delete empty lines at beginning and end (some email client insert these
# outside of the pgp signed message...)
if lines[0] == '' or lines[-1] == '':
from itertools import dropwhile
lines = list(dropwhile(lambda l: l == '', lines))
lines = list(reversed(list(dropwhile(lambda l: l == '', reversed(lines)))))
if len(lines) > 0 and lines[0] == '-----BEGIN PGP MESSAGE-----' \
and lines[-1] == '-----END PGP MESSAGE-----':
try:
sigs, d = crypto.decrypt_verify(message.encode('utf-8'))
message = decode(d)
except errors.GPGProblem:
pass
elif len(lines) > 0 and lines[0] == '-----BEGIN PGP SIGNED MESSAGE-----' \
and lines[-1] == '-----END PGP SIGNATURE-----':
# gpgme does not seem to be able to extract the plain text part of a signed message
import gnupg
gpg = gnupg.GPG()
d = gpg.decrypt(message.encode('utf8'))
message = d.data.decode('utf8')
quote_prefix = settings.get('quote_prefix')
return "\n".join([quote_prefix + line for line in message.splitlines()])
The debbugs bug tracking system as used by gnu.org and debian.org is controlled via email. By default messages to a bug result in an acknowledgement email. To prevent these emails, the X-Debbugs-No-Ack
header has to be added to the emails to the bug server. The following hook automatically adds this header to all emails to [email protected]:
from email.utils import getaddresses
def pre_envelope_send(ui, dbm, cmd):
e = ui.current_buffer.envelope
found = False
for header in ['To', 'Cc', 'Bcc']:
for _, address in getaddresses(e.get_all(header, [])):
if re.match(r'^\[email protected]$', address):
found = True
break
if found:
break
if found:
e.add("X-Debbugs-No-Ack", "kthxbye")
If you need to fetch your email and you've already configured notmuch hooks you may want to add a hook in alot in order to run notmuch new
and fetch the new email when you press a key.
First you need to add some code in alot/hooks.py
in order to define the function getmail
def getmail(ui=None):
ui.notify("fetchinig email..")
msg=subprocess.Popen("notmuch new".split(),stdout=subprocess.PIPE,stderr=subprocess.PIPE)
Then you can map a key in order to do get your emails just adding the following line in your alot/config
file
[bindings]
G = call hooks.getmail(ui)
As an alot developer I run alot directly from git. In order to know which version of the code I am currently running (helpful if you switch branches) I use this hook function in my inital_command
:
import alot
import os.path
import subprocess
def version_notification(ui):
dn = os.path.dirname
directory = dn(dn(alot.__file__))
output = lambda *x: subprocess.check_output(x, cwd=directory).strip()
commit = output('git', 'describe', '--tags')
branch = output('git', 'rev-parse', '--abbrev-ref', 'HEAD')
ui.notify('Version: {}\nGit commit: {}\nGit branch: {}'.format(
alot.__version__, commit, branch), timeout=10)
Useful for viewing and printing HTML emails as they are intended by the sender:
import alot
import tempfile
import webbrowser
from alot.helper import string_sanitize
from alot.helper import string_decode
# Helper method to extract the raw html part of a message. Note that it
# only extracts the first text/html part found.
def _get_raw_html(msg):
mail = msg.get_email()
for part in mail.walk():
ctype = part.get_content_type()
if ctype != "text/html":
continue
cd = part.get('Content-Disposition', '')
if cd.startswith('attachment'):
continue
enc = part.get_content_charset() or 'utf-8'
raw = string_decode(part.get_payload(decode=True), enc)
return string_sanitize(raw)
return None
# Opens HTML emails in an external browser.
# Related issue:
# - https://github.com/pazz/alot/issues/1153
def open_in_browser(ui=None):
ui.notify("Opening message in browser...")
msg = ui.current_buffer.get_selected_message()
htmlstr = _get_raw_html(msg)
if htmlstr == None:
ui.notify("Email has no html part")
return
temp = tempfile.NamedTemporaryFile(prefix="alot-",suffix=".html",
delete=False)
temp.write(htmlstr.encode("utf-8"))
temp.flush()
temp.close()
webbrowser.open(temp.name)
This piece of code implements collection of keys both as ascii-armored inline text and as attached MIME parts, and allows importing them into your main keyring.
A temporary keyring is used to parse the keys found (because gpgme doesn't allow for other forms of key parsing, it seems) and a confirmation is required for each key found. Improvements are very welcome. :-)
Add the following to .config/alot/hooks.py
:
import gpg
import os
import shutil
import signal
import subprocess
import tempfile
from contextlib import contextmanager
from twisted.internet import defer
BEGIN_KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----"
END_KEY = "-----END PGP PUBLIC KEY BLOCK-----"
def _get_inline_keys(content):
if BEGIN_KEY not in content:
return []
keys = []
while content:
start = content.find(BEGIN_KEY)
if start == -1:
# there are no more inline keys
break
content = content[start:]
end = content.find(END_KEY) + len(END_KEY)
key = content[0:end]
keys.append(key)
content = content[end:]
return keys
def _get_attached_keys(attachments):
keys = []
for attachment in attachments:
content_type = attachment.get_content_type()
if content_type == 'application/pgp-keys':
keys.append(attachment.get_data())
return keys
@contextmanager
def temp_gpg_context():
tempdir = tempfile.mkdtemp()
tempctx = gpg.Context()
tempctx.set_engine_info(gpg.constants.PROTOCOL_OpenPGP, home_dir=tempdir)
try:
yield tempctx
finally:
# Kill any gpg-agent's that have been opened
lookfor = 'gpg-agent --homedir {}'.format(tempdir)
out = subprocess.check_output(['ps', 'xo', 'pid,cmd'],
stderr=open('/dev/null', 'w'))
for each in out.strip().split('\n'):
pid, cmd = each.strip().split(' ', 1)
if cmd.startswith(lookfor):
os.kill(int(pid), signal.SIGKILL)
shutil.rmtree(tempdir)
@defer.inlineCallbacks
def import_keys(ui):
ui.notify('Looking for keys in message...')
m = ui.current_buffer.get_selected_message()
content = m.get_text_content()
attachments = m.get_attachments()
inline = _get_inline_keys(content)
attached = _get_attached_keys(attachments)
keys = inline + attached
if not keys:
ui.notify('No keys found in message.')
return
for keydata in keys:
with temp_gpg_context() as tempctx:
tempctx.op_import(keydata)
key = [k for k in tempctx.keylist()].pop()
fpr = key.fpr
uids = [u.uid for u in key.uids]
confirm = 'Found key %s with uids:' % fpr
for uid in uids:
confirm += '\n %s' % uid
confirm += '\nImport key into keyring?'
if (yield ui.choice(confirm, select='yes')) == 'yes':
# ***ATTENTION*** - operation in real keyring
ctx = gpg.Context()
ctx.op_import(keydata)
ui.notify('Key imported: %s' % fpr)
You can add a key binding to call the hook in .config/alot/config
as such:
[bindings]
[[thread]]
k = call hooks.import_keys(ui)
The following hooks allow for searching for key in your default keyring in different ways and attaching them to a message.
Things to improve:
- an alternative way to see the list of keys found and select one (or many) for attachment.
- give the option of attaching a minimal key (i.e. using
GPGME_EXPORT_MODE_MINIMAL
). - others?
import gpg
from email.mime.base import MIMEBase
from email.encoders import encode_base64
from email.utils import parseaddr
from twisted.internet import defer
from alot.db.attachment import Attachment
from alot.settings.const import settings
#
# Attach key
#
def _key_to_mime(ctx, fpr):
"""
Return an 'application/pgp-keys' MIME part containing an ascii-armored
OpenPGP public key.
"""
filename = '0x{}.pub.asc'.format(fpr)
key = gpg.Data()
ctx.op_export(fpr, 0, key)
key.seek(0, 0)
content = key.read()
part = MIMEBase('application', 'pgp-keys')
part.set_payload(content)
encode_base64(part)
part.add_header('Content-Disposition', 'attachment', filename=filename)
return part
def _attach_key(ui, pattern):
"""
Attach an OpenPGP public key to the current envelope.
"""
ctx = gpg.Context()
ctx.armor = True
keys = _list_keys(pattern)
for key in keys:
part = _key_to_mime(ctx, key.fpr)
attachment = Attachment(part)
ui.current_buffer.envelope.attachments.append(attachment)
ui.notify('Attached key %s' % key.fpr)
ui.current_buffer.rebuild()
def _list_keys(pattern):
"""
Return a list of OpenPGP keys that match the given pattern.
"""
ctx = gpg.Context()
ctx.armor = True
keys = [k for k in ctx.keylist(pattern)]
return keys
@defer.inlineCallbacks
def attach_keys(ui):
"""
Query the user for a pattern, search the default keyring, and offer to
attach matching keys.
"""
pattern = yield ui.prompt('Search for key to attach')
ui.notify('Looking for "{}" in keyring...'.format(pattern))
keys = _list_keys(pattern)
if not keys:
ui.notify('No keys found.')
return
for key in keys:
prompt = []
fpr = "{}".format(key.fpr)
prompt.append("Key 0x{}:".format(fpr))
for uid in key.uids:
prompt.append(" {}".format(uid.uid))
prompt.append('Attach?')
if (yield ui.choice('\n'.join(prompt), select='yes')) == 'yes':
_attach_key(ui, fpr)
def attach_my_key(ui):
"""
Attach my own OpenPGP public key to the current envelope.
"""
sender = ui.current_buffer.envelope.get('From', "")
address = parseaddr(sender)[1]
acc = settings.get_account_by_address(address)
fpr = acc.gpg_key.fpr
return _attach_key(ui, fpr)
def attach_recipient_keys(ui):
"""
Attach the OpenPGP public keys of all the recipients of the email.
"""
to = ui.current_buffer.envelope.get('To', "")
cc = ui.current_buffer.envelope.get('Cc', "")
for recipient in to.split(',') + cc.split(','):
address = parseaddr(recipient)[1]
if address:
_attach_key(ui, address)
You can add key bindings to call the hooks in .config/alot/config
as such:
[bindings]
[[envelope]]
k = call hooks.attach_my_key(ui)
K = call hooks.attach_keys(ui)
'control k' = call hooks.attach_recipient_keys(ui)
This hook allows you to bind a key to apply patches to different projects based on the notmuch tags of the email.
create a file called patch.config
alongside your main alot config (usually in ~/.config/alot/patch.config
), it is organized such that each section heading is a tag, and it has a single option directory
which points to the source directory of that repository:
[lists/alot]
directory = ~/src/alot
Add a binding such as:
[bindings]
[[thread]]
a = call hooks.apply_patch(ui)
# Copyright © 2017 Dylan Baker
#
# 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 3 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, see <http://www.gnu.org/licenses/>.
import logging
import os
import subprocess
from alot.settings.utils import read_config
def _get_config():
config_path = os.path.join(
os.environ.get('XDG_CONFIG_HOME', os.path.join(os.environ['HOME'], '.config')),
'alot', 'patch.config')
return read_config(configpath=config_path)
CONFIG = _get_config()
def apply_patch(ui):
message = ui.current_buffer.get_selected_message()
filename = message.get_filename()
for tag in message.get_tags():
if tag in CONFIG:
config = CONFIG[tag]
break
else:
logging.debug('found: ' + ', '.join(message.get_tags()))
ui.notify('No tags matched a config rule!', priority='error')
return
try:
subprocess.check_output(
['git', '-C', os.path.expanduser(config['directory']), 'am', '-3', filename],
stderr=subprocess.STDOUT)
except Exception as e:
ui.notify('Failed to apply patch. Reason:' + str(e), priority='error')
logging.debug('git am output: ' + e.output)
else:
ui.notify('Patch applied.')