Created
May 4, 2022 17:53
-
-
Save bonzini/7a59c4191c42e9ac9163d7a35dd13b76 to your computer and use it in GitHub Desktop.
gnome-sendmail.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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