Skip to content

Instantly share code, notes, and snippets.

@bonzini
Created May 4, 2022 17:53
Show Gist options
  • Save bonzini/7a59c4191c42e9ac9163d7a35dd13b76 to your computer and use it in GitHub Desktop.
Save bonzini/7a59c4191c42e9ac9163d7a35dd13b76 to your computer and use it in GitHub Desktop.
gnome-sendmail.py
#! /usr/bin/env python3
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Command line access to GNOME Online Accounts mail accounts
#
# Authors:
# Daniel P. Berrange <[email protected]>
# Paolo Bonzini <[email protected]>
import argparse
import base64
import smtplib
import sys
import json
import gi
gi.require_version("Gio", "2.0")
from gi.repository import GLib
from gi.repository import Gio
GOA_BUS_NAME = "org.gnome.OnlineAccounts"
GOA_BUS_PATH = "/org/gnome/OnlineAccounts"
GOA_ACCT_IFACE = "org.gnome.OnlineAccounts.Account"
GOA_MAIL_IFACE = "org.gnome.OnlineAccounts.Mail"
GOA_OAUTH2_IFACE = "org.gnome.OnlineAccounts.OAuth2Based"
GOA_PASSWORD_IFACE = "org.gnome.OnlineAccounts.PasswordBased"
def goa_dbus_proxy(path, iface):
return Gio.DBusProxy.new_for_bus_sync(
Gio.BusType.SESSION,
Gio.DBusProxyFlags.NONE,
None,
GOA_BUS_NAME,
path,
iface,
None,
)
class GOAMailAccount:
def __init__(self, obj_path, obj_ifaces):
self.obj_path = obj_path
self.obj_ifaces = obj_ifaces
@property
def path(self):
return obj_path
@property
def can_authenticate(self):
return (
GOA_OAUTH2_IFACE in self.obj_ifaces or GOA_PASSWORD_IFACE in self.obj_ifaces
)
@property
def identity(self):
return self.obj_ifaces[GOA_ACCT_IFACE]["Identity"]
@property
def is_oauth2(self):
return GOA_OAUTH2_IFACE in self.obj_ifaces
@property
def oauth2_access_token(self):
if GOA_OAUTH2_IFACE not in self.obj_ifaces:
raise Exception("not an OAuth2 account")
mgr = goa_dbus_proxy(self.obj_path, GOA_OAUTH2_IFACE)
token = mgr.call_sync("GetAccessToken", None, Gio.DBusCallFlags.NONE, -1, None)
return token.unpack()[0]
def auth_oauthbearer(self, challenge=None):
if GOA_OAUTH2_IFACE not in self.obj_ifaces:
raise Exception("not an OAuth2 account")
a = "\001"
token = self.oauth2_access_token
return f"n,a={self.smtp_user},{a}host={self.smtp_host}{a}port={self.smtp_port}{a}auth=Bearer {token}{a}{a}"
@property
def smtp_password(self):
if GOA_PASSWORD_IFACE not in self.obj_ifaces:
raise Exception("not a password-based account")
mgr = goa_dbus_proxy(self.obj_path, GOA_PASSWORD_IFACE)
arg = GLib.Variant.new_string("smtp-password")
args = GLib.Variant.new_tuple(arg)
password = mgr.call_sync("GetPassword", args, Gio.DBusCallFlags.NONE, -1, None)
return password.unpack()[0]
@property
def smtp_host(self):
return self.obj_ifaces[GOA_MAIL_IFACE]["SmtpHost"]
@property
def smtp_user(self):
return self.obj_ifaces[GOA_MAIL_IFACE]["SmtpUserName"]
@property
def smtp_port(self):
if self.obj_ifaces[GOA_MAIL_IFACE]["SmtpUseSsl"]:
return 465
if self.obj_ifaces[GOA_MAIL_IFACE]["SmtpUseTls"]:
return 587
return 25
def smtp_login(self):
if not self.obj_ifaces[GOA_MAIL_IFACE]["SmtpSupported"]:
raise Exception("SMTP not supported for this account")
if self.obj_ifaces[GOA_MAIL_IFACE]["SmtpUseSsl"]:
smtp = smtplib.SMTP_SSL(self.smtp_host, self.smtp_port)
else:
smtp = smtplib.SMTP(self.smtp_host, self.smtp_port)
if self.obj_ifaces[GOA_MAIL_IFACE]["SmtpUseTls"]:
smtp.ehlo()
smtp.starttls()
smtp.ehlo()
if GOA_OAUTH2_IFACE in self.obj_ifaces:
smtp.auth("OAUTHBEARER", self.auth_oauthbearer)
return smtp
if GOA_PASSWORD_IFACE in self.obj_ifaces:
smtp.login(self.smtp_user, self.smtp_password)
return smtp
raise Exception("do not know how to login")
def find_mail_account_objs():
mgr = goa_dbus_proxy(GOA_BUS_PATH, "org.freedesktop.DBus.ObjectManager")
accts = mgr.call_sync("GetManagedObjects", None, Gio.DBusCallFlags.NONE, -1, None)
acct_objs = accts.unpack()[0]
return [
GOAMailAccount(obj_path, obj_ifaces)
for obj_path, obj_ifaces in acct_objs.items()
if GOA_ACCT_IFACE in obj_ifaces and GOA_MAIL_IFACE in obj_ifaces
]
def list_mail_identities():
for obj in find_mail_account_objs():
if obj.can_authenticate:
print(obj.identity)
def get_mail_account_obj(identity):
for obj in find_mail_account_objs():
if obj.identity == identity:
return obj
return None
def main():
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description="""Command line access to GNOME Online Accounts mail accounts.""",
epilog="""
If invoked with a username as the sole argument, print the SMTP
password or the OAUTH2 authentication token.
If invoked with -f and -i arguments send email from the given
email address to the recipients, similar to /usr/bin/sendmail.
The -f argument is required in order to send email; if you are
using 'git send-email', please add:\n
[sendemail]
envelopesender = auto\n
to your git configuration""",
)
parser.add_argument(
"username",
metavar="USERNAME",
nargs="?",
help="Retrieve password or token for the user",
)
parser.add_argument(
"-f",
dest="sender",
metavar="SENDER",
help="Sender of the message"
)
parser.add_argument(
"-i",
dest="recipients",
metavar="RECIPIENT",
nargs="+",
help="Recipient of the message",
)
args = parser.parse_args()
if not args.sender and args.recipients:
print("The -f argument is required in order to send email.", file=sys.stderr)
print("If you are using 'git send-email', please add:\n", file=sys.stderr)
print(" [sendemail]", file=sys.stderr)
print(" envelopesender = auto\n", file=sys.stderr)
print("to your git configuration", file=sys.stderr)
sys.exit(1)
if args.sender and args.username:
print(f"Cannot specify a username together with -f.", file=sys.stderr)
sys.exit(1)
identity = args.username or args.sender
if not identity:
list_mail_identities()
sys.exit(0)
gmail_obj = get_mail_account_obj(identity)
if gmail_obj is None:
print(f"No GNOME account found for {identity} with Mail", file=sys.stderr)
sys.exit(1)
if args.username:
if gmail_obj.is_oauth2:
print(gmail_obj.oauth2_access_token)
else:
print(gmail_obj.smtp_password)
else:
smtp = gmail_obj.smtp_login()
msg = b"\r\n".join(sys.stdin.buffer.read().splitlines())
smtp.sendmail(args.sender, args.recipients, msg)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment