Add MMS sending capacilities #1

Merged
alex merged 12 commits from dev into master 2021-05-21 05:57:05 +00:00
2 changed files with 105 additions and 59 deletions
Showing only changes of commit 2446c76b6a - Show all commits

View file

@ -14,12 +14,11 @@ in case the mbox is locked by another process the output could be found in :
- python3 - python3
- python-messaging (pip install python-messaging) - python-messaging (pip install python-messaging)
- marrow.mailer (pip install marrow.mailer)
### setup ### setup
Install the dependency and mms2mail: Install the dependency and mms2mail:
``` ```
pip install --user marrow-mailer sudo apt-get install python3
pip install --user python-messaging pip install --user python-messaging
mkdir -p ~/.local/bin mkdir -p ~/.local/bin
@ -47,7 +46,8 @@ attach_mms = false ; whether to attach the full mms binary file
## usage ## usage
``` ```
mms2mail [-h] [-d | -f FILES [FILES ...]] [--delete] [--disable-dbus] [--force-read] [--force-unlock] mms2mail [-h] [-d | -f FILES [FILES ...]] [--delete] [--force-read]
[--force-unlock] [-l {critical,error,warning,info,debug}]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -55,7 +55,9 @@ optional arguments:
-f FILES [FILES ...], --file FILES [FILES ...] -f FILES [FILES ...], --file FILES [FILES ...]
Parse specified mms files and quit Parse specified mms files and quit
--delete Ask mmsd to delete the converted MMS --delete Ask mmsd to delete the converted MMS
--disable-dbus disable dbus request to mmsd
--force-read Force conversion even if MMS is marked as read --force-read Force conversion even if MMS is marked as read
--force-unlock BEWARE COULD LEAD TO WHOLE MBOX CORRUPTION Force unlocking the mbox after a few minutes /!\ --force-unlock BEWARE COULD LEAD TO WHOLE MBOX CORRUPTION Force unlocking the
mbox after a few minutes /!\
-l {critical,error,warning,info,debug}, --logging {critical,error,warning,info,debug}
Define the logger output level
``` ```

138
mms2mail
View file

@ -29,17 +29,18 @@ import getpass
import socket import socket
import mimetypes import mimetypes
import time import time
import logging
from pathlib import Path from pathlib import Path
from messaging.mms.message import MMSMessage from messaging.mms.message import MMSMessage
import mailbox import mailbox
from marrow.mailer import Message import email
from gi.repository import GLib from gi.repository import GLib
import dbus import dbus
import dbus.mainloop.glib import dbus.mainloop.glib
log = __import__('logging').getLogger(__name__) log = logging.getLogger(__name__)
class MMS2Mail: class MMS2Mail:
@ -51,7 +52,7 @@ class MMS2Mail:
""" """
def __init__(self, delete=False, force_read=False, def __init__(self, delete=False, force_read=False,
disable_dbus=False, force_unlock=False): force_unlock=False):
""" """
Return class instance. Return class instance.
@ -69,7 +70,6 @@ class MMS2Mail:
""" """
self.delete = delete self.delete = delete
self.force_read = force_read self.force_read = force_read
self.disable_dbus = disable_dbus
self.force_unlock = force_unlock self.force_unlock = force_unlock
self.config = configparser.ConfigParser() self.config = configparser.ConfigParser()
self.config.read(f"{Path.home()}/.mms/modemmanager/mms2mail.ini") self.config.read(f"{Path.home()}/.mms/modemmanager/mms2mail.ini")
@ -98,37 +98,35 @@ class MMS2Mail:
:param path: the mms filesystem path :param path: the mms filesystem path
:type path: str :type path: str
:rtype bool
:return the mms status or None
:rtype str
""" """
# Check for mmsd data file # Check for mmsd data file
if not Path(f"{path}").is_file(): if not Path(f"{path}").is_file():
log.error("MMS file not found : aborting") log.error("MMS file not found : aborting")
return False return None
# Check for mmsd status file # Check for mmsd status file
status = configparser.ConfigParser() status = configparser.ConfigParser()
if not Path(f"{path}.status").is_file(): if not Path(f"{path}.status").is_file():
log.error("MMS status file not found : aborting") log.error("MMS status file not found : aborting")
return False return None
status.read_file(open(f"{path}.status")) status.read_file(open(f"{path}.status"))
# Allow only incoming MMS for the time beeing
if not (status['info']['state'] == 'downloaded' or
status['info']['state'] == 'received'):
log.error("Outgoing MMS : aborting")
return False
if not (self.force_read or not status.getboolean('info', 'read')): if not (self.force_read or not status.getboolean('info', 'read')):
log.error("Already converted MMS : aborting") log.error("Already converted MMS : aborting")
return False return None
return True return status['info']['state']
def message_added(self, name, value, member, path, interface): def message_added(self, name, value, member, path, interface):
"""Trigger conversion on MessageAdded signal.""" """Trigger conversion on MessageAdded signal."""
if value['Status'] == 'downloaded' or value['Status'] == 'received': if value['Status'] == 'downloaded' or value['Status'] == 'received':
log.debug(f"New incoming MMS found ({name.split('/')[-1]})") log.debug(f"New incoming MMS found ({name.split('/')[-1]})")
self.convert(value['Attachments'][0][2], name) self.convert(path=value['Attachments'][0][2], dbus_path=name,
properties=value)
else: else:
log.debug(f"New outgoing MMS found ({name.split('/')[-1]})") log.debug(f"New outgoing MMS found ({name.split('/')[-1]})")
def convert(self, path, dbus_path=None): def convert(self, path, dbus_path=None, properties=None):
""" """
Convert a provided mms file to a mail stored in a mbox. Convert a provided mms file to a mail stored in a mbox.
@ -139,7 +137,8 @@ class MMS2Mail:
:type dbus_path: str :type dbus_path: str
""" """
# Check if the provided file present # Check if the provided file present
if not self.check_mms(path): status = self.check_mms(path)
if not status:
log.error("MMS file not convertible.") log.error("MMS file not convertible.")
return return
# Generate its dbus path, for future operation (mark as read, delete) # Generate its dbus path, for future operation (mark as read, delete)
@ -147,48 +146,83 @@ class MMS2Mail:
dbus_path = f"/org/ofono/mms/modemmanager/{path.split('/')[-1]}" dbus_path = f"/org/ofono/mms/modemmanager/{path.split('/')[-1]}"
mms = MMSMessage.from_file(path) mms = MMSMessage.from_file(path)
message = email.message.EmailMessage()
message = Message()
# Generate Mail Headers # Generate Mail Headers
mms_from, mms_from_type = mms.headers.get('From', mms_h_from = mms.headers.get('From', 'unknown/undef')
'unknown/undef').split('/') log.debug(f"MMS[From]: {mms_h_from}")
message.author = f"{mms_from}@{self.domain}" if 'not inserted' in mms_h_from:
mms_from, mms_from_type = mms.headers.get('To', mms_h_from = 'unknown/undef'
'unknown/undef').split('/') mms_from, mms_from_type = mms_h_from.split('/')
message.to = f"{self.user}@{self.domain}" 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
if 'Subject' in mms.headers and mms.headers['Subject']: if 'Subject' in mms.headers and mms.headers['Subject']:
message.subject = mms.headers['Subject'] message['Subject'] = mms.headers['Subject']
else: else:
message.subject = f"MMS from {mms_from}" 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']: if 'Date' in mms.headers and mms.headers['Date']:
message.date = mms.headers['Date'] message['Date'] = mms.headers['Date']
# Recopy MMS HEADERS # Recopy MMS HEADERS
for header in mms.headers: for header in mms.headers:
message.headers.append((f"X-MMS-{header}", message.add_header(f"X-MMS-{header}", f"{mms.headers[header]}")
f"{mms.headers[header]}"))
message.plain = " " message.preamble = "This mail is converted from a MMS."
body = ""
data_id = 1 data_id = 1
attachments = []
for data_part in mms.data_parts: for data_part in mms.data_parts:
datacontent = data_part.headers['Content-Type'] datacontent = data_part.headers['Content-Type']
if datacontent is not None: if datacontent is not None:
maintype, subtype = datacontent[0].split('/', 1)
if 'text/plain' in datacontent[0]: if 'text/plain' in datacontent[0]:
encoding = datacontent[1].get('Charset', 'utf-8') encoding = datacontent[1].get('Charset', 'utf-8')
plain = data_part.data.decode(encoding) body += data_part.data.decode(encoding) + '\n'
message.plain += plain + '\n'
continue continue
extension = str(mimetypes.guess_extension(datacontent[0])) extension = str(mimetypes.guess_extension(datacontent[0]))
filename = datacontent[1].get('Name', str(data_id)) filename = datacontent[1].get('Name', str(data_id))
message.attach(filename + extension, data_part.data) attachments.append([data_part.data, maintype,
subtype, filename + extension])
data_id = data_id + 1 data_id = data_id + 1
if body:
message.set_content(body)
for a in attachments:
message.add_attachment(a[0],
maintype=a[1],
subtype=a[2],
filename=a[3])
# Add MMS binary file, for debugging purpose or reparsing in the future # Add MMS binary file, for debugging purpose or reparsing in the future
if self.attach_mms: if self.attach_mms:
message.attach(path, None, None, None, False, path.split('/')[-1]) 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 # Write the mail in case of mbox lock retry for 5 minutes
# Ultimately write in an mbox in the home folder # Ultimately write in an mbox in the home folder
@ -197,7 +231,7 @@ class MMS2Mail:
try: try:
# self.mailer.send(message) # self.mailer.send(message)
self.mailbox.lock() self.mailbox.lock()
self.mailbox.add(mailbox.mboxMessage(str(message))) self.mailbox.add(mailbox.mboxMessage(message))
self.mailbox.flush() self.mailbox.flush()
self.mailbox.unlock() self.mailbox.unlock()
break break
@ -228,10 +262,8 @@ class MMS2Mail:
else: else:
time.sleep(5) time.sleep(5)
# Ask mmsd to mark message as read and delete it # Ask mmsd to mark message as read and delete it
if self.disable_dbus: if properties:
return
self.dbus.mark_mms_read(dbus_path) self.dbus.mark_mms_read(dbus_path)
if self.delete: if self.delete:
self.dbus.delete_mms(dbus_path) self.dbus.delete_mms(dbus_path)
@ -267,7 +299,7 @@ class DbusMMSd():
:param dbus_path: the mms dbus path :param dbus_path: the mms dbus path
:type dbus_path: str :type dbus_path: str
""" """
message = dbus.Interface(self.bus.get_object('org.ofono.mms', message = dbus.proxies.Interface(self.bus.get_object('org.ofono.mms',
dbus_path), dbus_path),
'org.ofono.mms.Message') 'org.ofono.mms.Message')
log.debug(f"Marking MMS as read {dbus_path}") log.debug(f"Marking MMS as read {dbus_path}")
@ -282,7 +314,7 @@ class DbusMMSd():
""" """
if self.disable_dbus: if self.disable_dbus:
return None return None
message = dbus.Interface(self.bus.get_object('org.ofono.mms', message = dbus.proxies.Interface(self.bus.get_object('org.ofono.mms',
dbus_path), dbus_path),
'org.ofono.mms.Message') 'org.ofono.mms.Message')
log.debug(f"Deleting MMS {dbus_path}") log.debug(f"Deleting MMS {dbus_path}")
@ -323,9 +355,6 @@ def main():
help="Parse specified mms files and quit", dest='files') help="Parse specified mms files and quit", dest='files')
parser.add_argument('--delete', action='store_true', dest='delete', parser.add_argument('--delete', action='store_true', dest='delete',
help="Ask mmsd to delete the converted MMS") help="Ask mmsd to delete the converted MMS")
parser.add_argument('--disable-dbus', action='store_true',
dest='disable_dbus',
help="disable dbus request to mmsd")
parser.add_argument('--force-read', action='store_true', parser.add_argument('--force-read', action='store_true',
dest='force_read', help="Force conversion even if MMS \ dest='force_read', help="Force conversion even if MMS \
is marked as read") is marked as read")
@ -334,16 +363,31 @@ def main():
WHOLE MBOX CORRUPTION \ WHOLE MBOX CORRUPTION \
Force unlocking the mbox \ Force unlocking the mbox \
after a few minutes /!\\") after a few minutes /!\\")
parser.add_argument('-l', '--logging', dest='log_level', default='warning',
choices=['critical', 'error', 'warning',
'info', 'debug'],
help='Define the logger output level'
)
args = parser.parse_args() args = parser.parse_args()
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() d = DbusMMSd()
m = MMS2Mail(args.delete, args.force_read, m = MMS2Mail(delete=args.delete, force_read=args.force_read,
args.disable_dbus, args.force_unlock) force_unlock=args.force_unlock)
m.set_dbus(d) m.set_dbus(d)
if args.files: if args.files:
for mms_file in args.files: for mms_file in args.files:
m.convert(mms_file) m.convert(path=mms_file)
return
elif args.watcher: elif args.watcher:
log.info("Starting mms2mail in daemon mode") log.info("Starting mms2mail in daemon mode")
d.set_mms2mail(m) d.set_mms2mail(m)