mms2mail/mms2mail

379 lines
14 KiB
Text
Raw Normal View History

2021-05-01 06:43:13 +00:00
#!/usr/bin/python3
2021-05-05 03:49:38 +00:00
"""An mms to mail converter for mmsd."""
2021-05-01 06:43:13 +00:00
import argparse
import configparser
import getpass
import socket
2021-05-06 20:12:10 +00:00
import mimetypes
import time
2021-05-10 08:30:37 +00:00
import logging
2021-05-01 06:43:13 +00:00
from pathlib import Path
2021-05-01 06:43:13 +00:00
from messaging.mms.message import MMSMessage
import mailbox
2021-05-10 08:30:37 +00:00
import email
2021-05-01 06:43:13 +00:00
2021-05-05 03:49:38 +00:00
from gi.repository import GLib
import dbus
import dbus.mainloop.glib
2021-05-01 06:43:13 +00:00
2021-05-10 08:30:37 +00:00
log = logging.getLogger(__name__)
2021-05-05 03:49:38 +00:00
2021-05-01 06:43:13 +00:00
class MMS2Mail:
"""
2021-05-05 03:49:38 +00:00
The class handling the conversion between MMS and mail format.
2021-05-01 06:43:13 +00:00
MMS support is provided by python-messaging
Mail support is provided by marrow.mailer
"""
2021-05-05 03:49:38 +00:00
def __init__(self, delete=False, force_read=False,
2021-05-10 08:30:37 +00:00
force_unlock=False):
"""
Return class instance.
:param delete: delete MMS after conversion
:type delete: bool
:param force_read: force converting an already read MMS (batch mode)
:type force_read: bool
:param disable_dbus: Disable sending dbus commands to mmsd (batch mode)
:type disable_dbus: bool
:param force_unlock: Force mbox unlocking after a few minutes
:type force_unlock: bool
"""
self.delete = delete
self.force_read = force_read
self.force_unlock = force_unlock
2021-05-01 06:43:13 +00:00
self.config = configparser.ConfigParser()
self.config.read(f"{Path.home()}/.mms/modemmanager/mms2mail.ini")
self.attach_mms = self.config.getboolean('mail', 'attach_mms',
fallback=False)
2021-05-05 03:49:38 +00:00
self.domain = self.config.get('mail', 'domain',
fallback=socket.getfqdn())
self.user = self.config.get('mail', 'user', fallback=getpass.getuser())
mbox_file = self.config.get('mail', 'mailbox',
fallback=f"/var/mail/{self.user}")
self.mailbox = mailbox.mbox(mbox_file)
self.dbus = None
2021-05-01 06:43:13 +00:00
def set_dbus(self, dbusmmsd):
"""
2021-05-05 03:49:38 +00:00
Return the DBus SessionBus.
:param dbusmmsd: The DBus MMSd abstraction class
:type dbusmmsd: DbusMMSd()
"""
self.dbus = dbusmmsd
def check_mms(self, path):
"""
Check wether the provided file would be converted.
:param path: the mms filesystem path
:type path: str
2021-05-10 08:30:37 +00:00
:return the mms status or None
:rtype str
"""
# Check for mmsd data file
if not Path(f"{path}").is_file():
log.error("MMS file not found : aborting")
2021-05-10 08:30:37 +00:00
return None
# Check for mmsd status file
2021-05-01 06:43:13 +00:00
status = configparser.ConfigParser()
if not Path(f"{path}.status").is_file():
log.error("MMS status file not found : aborting")
2021-05-10 08:30:37 +00:00
return None
2021-05-01 06:43:13 +00:00
status.read_file(open(f"{path}.status"))
if not (self.force_read or not status.getboolean('info', 'read')):
log.error("Already converted MMS : aborting")
2021-05-10 08:30:37 +00:00
return None
return status['info']['state']
def message_added(self, name, value, member, path, interface):
"""Trigger conversion on MessageAdded signal."""
if value['Status'] == 'downloaded' or value['Status'] == 'received':
log.debug(f"New incoming MMS found ({name.split('/')[-1]})")
2021-05-10 08:30:37 +00:00
self.convert(path=value['Attachments'][0][2], dbus_path=name,
properties=value)
else:
log.debug(f"New outgoing MMS found ({name.split('/')[-1]})")
2021-05-10 08:30:37 +00:00
def convert(self, path, dbus_path=None, properties=None):
"""
Convert a provided mms file to a mail stored in a mbox.
:param path: the mms filesystem path
:type path: str
:param dbus_path: the mms dbus path
:type dbus_path: str
"""
# Check if the provided file present
2021-05-10 08:30:37 +00:00
status = self.check_mms(path)
if not status:
log.error("MMS file not convertible.")
2021-05-01 06:43:13 +00:00
return
# Generate its dbus path, for future operation (mark as read, delete)
if not dbus_path:
dbus_path = f"/org/ofono/mms/modemmanager/{path.split('/')[-1]}"
2021-05-01 06:43:13 +00:00
2021-05-01 21:11:15 +00:00
mms = MMSMessage.from_file(path)
2021-05-10 08:30:37 +00:00
message = email.message.EmailMessage()
2021-05-01 06:43:13 +00:00
# Generate Mail Headers
2021-05-10 08:30:37 +00:00
mms_h_from = mms.headers.get('From', 'unknown/undef')
log.debug(f"MMS[From]: {mms_h_from}")
if 'not inserted' in mms_h_from:
mms_h_from = 'unknown/undef'
mms_from, mms_from_type = mms_h_from.split('/')
message['From'] = f"{mms_from}@{self.domain}"
mms_h_to = mms.headers.get('To', 'unknown/undef')
log.debug(f"MMS[To]: {mms_h_to}")
if 'not inserted' in mms_h_to:
mms_h_to = 'unknown/undef'
mms_to, mms_to_type = mms_h_to.split('/')
message['To'] = f"{mms_to}@{self.domain}"
# Get other recipients from dbus signal
# https://github.com/pmarti/python-messaging/issues/49
if properties:
cc = ""
for r in properties['Recipients']:
if mms_to in r:
continue
log.debug(f'MMS/MAIL CC : {r}')
cc += f"{r}@{self.domain},"
if cc:
cc = cc[:-1]
message['CC'] = cc
2021-05-01 21:11:15 +00:00
if 'Subject' in mms.headers and mms.headers['Subject']:
2021-05-10 08:30:37 +00:00
message['Subject'] = mms.headers['Subject']
2021-05-05 03:49:38 +00:00
else:
2021-05-10 08:30:37 +00:00
if status == 'sent' or status == 'draft':
message['Subject'] = f"MMS to {mms_to}"
else:
message['Subject'] = f"MMS from {mms_from}"
if 'Date' in mms.headers and mms.headers['Date']:
2021-05-10 08:30:37 +00:00
message['Date'] = mms.headers['Date']
2021-05-01 21:11:15 +00:00
# Recopy MMS HEADERS
for header in mms.headers:
2021-05-10 08:30:37 +00:00
message.add_header(f"X-MMS-{header}", f"{mms.headers[header]}")
2021-05-05 03:49:38 +00:00
2021-05-10 08:30:37 +00:00
message.preamble = "This mail is converted from a MMS."
body = ""
2021-05-06 20:12:10 +00:00
data_id = 1
2021-05-10 08:30:37 +00:00
attachments = []
2021-05-01 06:43:13 +00:00
for data_part in mms.data_parts:
2021-05-05 03:49:38 +00:00
datacontent = data_part.headers['Content-Type']
2021-05-01 06:43:13 +00:00
if datacontent is not None:
2021-05-10 08:30:37 +00:00
maintype, subtype = datacontent[0].split('/', 1)
2021-05-01 06:43:13 +00:00
if 'text/plain' in datacontent[0]:
2021-05-06 20:12:10 +00:00
encoding = datacontent[1].get('Charset', 'utf-8')
2021-05-10 08:30:37 +00:00
body += data_part.data.decode(encoding) + '\n'
2021-05-06 20:12:10 +00:00
continue
extension = str(mimetypes.guess_extension(datacontent[0]))
2021-05-06 20:12:10 +00:00
filename = datacontent[1].get('Name', str(data_id))
2021-05-10 08:30:37 +00:00
attachments.append([data_part.data, maintype,
subtype, filename + extension])
2021-05-06 20:12:10 +00:00
data_id = data_id + 1
2021-05-10 08:30:37 +00:00
if body:
message.set_content(body)
for a in attachments:
message.add_attachment(a[0],
maintype=a[1],
subtype=a[2],
filename=a[3])
2021-05-06 20:12:10 +00:00
2021-05-05 03:49:38 +00:00
# Add MMS binary file, for debugging purpose or reparsing in the future
if self.attach_mms:
2021-05-10 08:30:37 +00:00
with open(path, 'rb') as fp:
message.add_attachment(fp.read(),
maintype='application',
subtype='octet-stream',
filename=path.split('/')[-1])
# Write the mail in case of mbox lock retry for 5 minutes
# Ultimately write in an mbox in the home folder
end_time = time.time() + (5 * 60)
while True:
try:
# self.mailer.send(message)
self.mailbox.lock()
2021-05-10 08:30:37 +00:00
self.mailbox.add(mailbox.mboxMessage(message))
self.mailbox.flush()
self.mailbox.unlock()
break
except (mailbox.ExternalClashError, FileExistsError) as e:
log.warn(f"Exception Mbox lock : {e}")
if time.time() > end_time:
if self.force_unlock:
log.error("Force removing lock")
self.mailbox.unlock()
else:
fs_mbox_path = f"{Path.home()}/.mms/failsafembox"
fs_mbox = mailbox.mbox(fs_mbox_path)
log.warning(f"Writing in internal mbox {fs_mbox_path}")
try:
fs_mbox.unlock()
fs_mbox.lock()
fs_mbox.add(mailbox.mboxMessage(str(message)))
fs_mbox.flush()
fs_mbox.unlock()
break
except (mailbox.ExternalClashError,
FileExistsError) as e:
log.error(f"Failsafe Mbox error : {e}")
log.error(f"MMS cannot be written to any mbox : \
{path.split('/')[-1]}")
finally:
break
else:
time.sleep(5)
# Ask mmsd to mark message as read and delete it
2021-05-10 08:30:37 +00:00
if properties:
self.dbus.mark_mms_read(dbus_path)
if self.delete:
self.dbus.delete_mms(dbus_path)
2021-05-05 03:49:38 +00:00
class DbusMMSd():
"""Use DBus communication with mmsd."""
def __init__(self, mms2mail=None):
"""
2021-05-07 10:12:08 +00:00
Return a DBusWatcher instance.
2021-05-07 10:12:08 +00:00
:param mms2mail: An mms2mail instance to convert new mms
:type mms2mail: mms2mail()
"""
2021-05-07 10:12:08 +00:00
self.mms2mail = mms2mail
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
self.bus = dbus.SessionBus()
def set_mms2mail(self, mms2mail):
"""
Set mms2mail instance handling dbus event.
:param mms2mail: An mms2mail instance to convert new mms
:type mms2mail: mms2mail()
"""
self.mms2mail = mms2mail
def mark_mms_read(self, dbus_path):
"""
Ask mmsd to mark the mms as read.
:param dbus_path: the mms dbus path
:type dbus_path: str
"""
2021-05-10 08:30:37 +00:00
message = dbus.proxies.Interface(self.bus.get_object('org.ofono.mms',
dbus_path),
'org.ofono.mms.Message')
log.debug(f"Marking MMS as read {dbus_path}")
message.MarkRead()
def delete_mms(self, dbus_path):
"""
Ask mmsd to delete the mms.
:param dbus_path: the mms dbus path
:type dbus_path: str
"""
if self.disable_dbus:
return None
2021-05-10 08:30:37 +00:00
message = dbus.proxies.Interface(self.bus.get_object('org.ofono.mms',
dbus_path),
'org.ofono.mms.Message')
log.debug(f"Deleting MMS {dbus_path}")
message.Delete()
def add_signal_receiver(self):
"""Add a signal receiver to the current bus."""
if self.mms2mail:
self.bus.add_signal_receiver(self.mms2mail.message_added,
bus_name="org.ofono.mms",
signal_name="MessageAdded",
member_keyword="member",
path_keyword="path",
interface_keyword="interface")
return True
else:
return False
2021-05-05 03:49:38 +00:00
def run(self):
"""Run the dbus mainloop."""
mainloop = GLib.MainLoop()
log.info("Starting DBus watcher mainloop")
2021-05-07 10:12:08 +00:00
try:
mainloop.run()
except KeyboardInterrupt:
log.info("Stopping DBus watcher mainloop")
2021-05-07 10:12:08 +00:00
mainloop.quit()
2021-05-05 03:49:38 +00:00
2021-05-07 10:12:08 +00:00
def main():
"""Run the different functions handling mms and mail."""
2021-05-01 06:43:13 +00:00
parser = argparse.ArgumentParser()
mode = parser.add_mutually_exclusive_group()
2021-05-05 03:49:38 +00:00
mode.add_argument("-d", "--daemon",
2021-05-07 10:12:08 +00:00
help="Use dbus signal from mmsd to trigger conversion",
action='store_true', dest='watcher')
2021-05-05 03:49:38 +00:00
mode.add_argument("-f", "--file", nargs='+',
help="Parse specified mms files and quit", dest='files')
parser.add_argument('--delete', action='store_true', dest='delete',
help="Ask mmsd to delete the converted MMS")
parser.add_argument('--force-read', action='store_true',
dest='force_read', help="Force conversion even if MMS \
is marked as read")
parser.add_argument('--force-unlock', action='store_true',
dest='force_unlock', help="BEWARE COULD LEAD TO \
WHOLE MBOX CORRUPTION \
Force unlocking the mbox \
after a few minutes /!\\")
2021-05-10 08:30:37 +00:00
parser.add_argument('-l', '--logging', dest='log_level', default='warning',
choices=['critical', 'error', 'warning',
'info', 'debug'],
help='Define the logger output level'
)
2021-05-01 06:43:13 +00:00
args = parser.parse_args()
2021-05-10 08:30:37 +00:00
log.setLevel(args.log_level.upper())
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
formatter = logging.Formatter(log_format)
ch.setFormatter(formatter)
log.addHandler(ch)
d = DbusMMSd()
2021-05-10 08:30:37 +00:00
m = MMS2Mail(delete=args.delete, force_read=args.force_read,
force_unlock=args.force_unlock)
m.set_dbus(d)
2021-05-05 03:49:38 +00:00
if args.files:
for mms_file in args.files:
2021-05-10 08:30:37 +00:00
m.convert(path=mms_file)
return
2021-05-07 10:12:08 +00:00
elif args.watcher:
log.info("Starting mms2mail in daemon mode")
d.set_mms2mail(m)
d.add_signal_receiver()
2021-05-01 06:43:13 +00:00
else:
parser.print_help()
d.run()
2021-05-07 10:12:08 +00:00
if __name__ == '__main__':
main()