Add MMS sending capacilities #1
2 changed files with 105 additions and 59 deletions
12
README.md
12
README.md
|
@ -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
|
||||||
```
|
```
|
152
mms2mail
152
mms2mail
|
@ -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,13 +262,11 @@ 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)
|
|
||||||
|
|
||||||
|
|
||||||
class DbusMMSd():
|
class DbusMMSd():
|
||||||
|
@ -267,9 +299,9 @@ 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}")
|
||||||
message.MarkRead()
|
message.MarkRead()
|
||||||
|
|
||||||
|
@ -282,9 +314,9 @@ 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}")
|
||||||
message.Delete()
|
message.Delete()
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue