Add MMS sending capacilities #1
2 changed files with 98 additions and 33 deletions
18
README.md
18
README.md
|
@ -1,20 +1,24 @@
|
||||||
# mms2mail
|
# mms2mail
|
||||||
|
|
||||||
Convert MMSd MMS file to mbox.
|
Convert mmsd MMS file to mbox.
|
||||||
|
|
||||||
|
mms2mail can convert mms in batch mode, or wait for new mms via a dbus signal
|
||||||
|
sent by mmsd.
|
||||||
|
By default it store them in the current user mbox (```/var/mail/$USER```)
|
||||||
|
in case the mbox is locked by another process the output could be found in :
|
||||||
|
```$HOME/.mms/failsafembox```
|
||||||
|
|
||||||
## installation
|
## installation
|
||||||
|
|
||||||
### dependency
|
### dependency
|
||||||
|
|
||||||
- python3
|
- python3
|
||||||
- python3-watchdog (pip install watchdog)
|
|
||||||
- python-messaging (pip install python-messaging)
|
- python-messaging (pip install python-messaging)
|
||||||
- marrow.mailer (pip install marrow.mailer)
|
- marrow.mailer (pip install marrow.mailer)
|
||||||
|
|
||||||
### setup
|
### setup
|
||||||
Install the dependency and mms2mail:
|
Install the dependency and mms2mail:
|
||||||
```
|
```
|
||||||
pip install --user watchdog # or sudo apt install python3-watchdog
|
|
||||||
pip install --user marrow-mailer
|
pip install --user marrow-mailer
|
||||||
pip install --user python-messaging
|
pip install --user python-messaging
|
||||||
|
|
||||||
|
@ -42,16 +46,16 @@ attach_mms = false ; whether to attach the full mms binary file
|
||||||
```
|
```
|
||||||
|
|
||||||
## usage
|
## usage
|
||||||
|
|
||||||
```
|
```
|
||||||
mms2mail [-h] [-d [{dbus,filesystem}] | -f FILES [FILES ...]] [--delete] [--force-read]
|
mms2mail [-h] [-d | -f FILES [FILES ...]] [--delete] [--disable-dbus] [--force-read] [--force-unlock]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-d [{dbus,filesystem}], --daemon [{dbus,filesystem}]
|
-d, --daemon Use dbus signal from mmsd to trigger conversion
|
||||||
Use dbus signal from mmsd by default but can also watch mmsd storage folder (useful for mmsd < 1.0)
|
|
||||||
-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 /!\
|
||||||
```
|
```
|
113
mms2mail
113
mms2mail
|
@ -28,15 +28,19 @@ import configparser
|
||||||
import getpass
|
import getpass
|
||||||
import socket
|
import socket
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from messaging.mms.message import MMSMessage
|
from messaging.mms.message import MMSMessage
|
||||||
from marrow.mailer import Mailer, Message
|
import mailbox
|
||||||
|
from marrow.mailer import Message
|
||||||
|
|
||||||
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__)
|
||||||
|
|
||||||
|
|
||||||
class MMS2Mail:
|
class MMS2Mail:
|
||||||
"""
|
"""
|
||||||
|
@ -46,7 +50,8 @@ class MMS2Mail:
|
||||||
Mail support is provided by marrow.mailer
|
Mail support is provided by marrow.mailer
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, delete=False, force_read=False):
|
def __init__(self, delete=False, force_read=False,
|
||||||
|
disable_dbus=False, force_unlock=False):
|
||||||
"""
|
"""
|
||||||
Return class instance.
|
Return class instance.
|
||||||
|
|
||||||
|
@ -55,9 +60,17 @@ class MMS2Mail:
|
||||||
|
|
||||||
:param force_read: force converting an already read MMS (batch mode)
|
:param force_read: force converting an already read MMS (batch mode)
|
||||||
:type force_read: bool
|
: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.delete = delete
|
||||||
self.force_read = force_read
|
self.force_read = force_read
|
||||||
|
self.disable_dbus = disable_dbus
|
||||||
|
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")
|
||||||
self.attach_mms = self.config.getboolean('mail', 'attach_mms',
|
self.attach_mms = self.config.getboolean('mail', 'attach_mms',
|
||||||
|
@ -67,9 +80,9 @@ class MMS2Mail:
|
||||||
self.user = self.config.get('mail', 'user', fallback=getpass.getuser())
|
self.user = self.config.get('mail', 'user', fallback=getpass.getuser())
|
||||||
mbox_file = self.config.get('mail', 'mailbox',
|
mbox_file = self.config.get('mail', 'mailbox',
|
||||||
fallback=f"/var/mail/{self.user}")
|
fallback=f"/var/mail/{self.user}")
|
||||||
self.mailer = Mailer({'manager.use': 'immediate',
|
self.mailbox = mailbox.mbox(mbox_file)
|
||||||
'transport.use': 'mbox',
|
if self.disable_dbus:
|
||||||
'transport.file': mbox_file})
|
return
|
||||||
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
|
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
|
||||||
self.bus = dbus.SessionBus()
|
self.bus = dbus.SessionBus()
|
||||||
|
|
||||||
|
@ -80,6 +93,8 @@ class MMS2Mail:
|
||||||
:rtype dbus.SessionBus()
|
:rtype dbus.SessionBus()
|
||||||
:return: an active SessionBus
|
:return: an active SessionBus
|
||||||
"""
|
"""
|
||||||
|
if self.disable_dbus:
|
||||||
|
return None
|
||||||
return self.bus
|
return self.bus
|
||||||
|
|
||||||
def mark_mms_read(self, dbus_path):
|
def mark_mms_read(self, dbus_path):
|
||||||
|
@ -89,10 +104,12 @@ class MMS2Mail:
|
||||||
:param dbus_path: the mms dbus path
|
:param dbus_path: the mms dbus path
|
||||||
:type dbus_path: str
|
:type dbus_path: str
|
||||||
"""
|
"""
|
||||||
|
if self.disable_dbus:
|
||||||
|
return None
|
||||||
message = dbus.Interface(self.bus.get_object('org.ofono.mms',
|
message = dbus.Interface(self.bus.get_object('org.ofono.mms',
|
||||||
dbus_path),
|
dbus_path),
|
||||||
'org.ofono.mms.Message')
|
'org.ofono.mms.Message')
|
||||||
print(f"Marking MMS as read {dbus_path}", file=sys.stderr)
|
log.debug(f"Marking MMS as read {dbus_path}")
|
||||||
message.MarkRead()
|
message.MarkRead()
|
||||||
|
|
||||||
def delete_mms(self, dbus_path):
|
def delete_mms(self, dbus_path):
|
||||||
|
@ -102,10 +119,12 @@ class MMS2Mail:
|
||||||
:param dbus_path: the mms dbus path
|
:param dbus_path: the mms dbus path
|
||||||
:type dbus_path: str
|
:type dbus_path: str
|
||||||
"""
|
"""
|
||||||
|
if self.disable_dbus:
|
||||||
|
return None
|
||||||
message = dbus.Interface(self.bus.get_object('org.ofono.mms',
|
message = dbus.Interface(self.bus.get_object('org.ofono.mms',
|
||||||
dbus_path),
|
dbus_path),
|
||||||
'org.ofono.mms.Message')
|
'org.ofono.mms.Message')
|
||||||
print(f"Deleting MMS {dbus_path}", file=sys.stderr)
|
log.debug(f"Deleting MMS {dbus_path}")
|
||||||
message.Delete()
|
message.Delete()
|
||||||
|
|
||||||
def check_mms(self, path):
|
def check_mms(self, path):
|
||||||
|
@ -118,21 +137,21 @@ class MMS2Mail:
|
||||||
"""
|
"""
|
||||||
# Check for mmsd data file
|
# Check for mmsd data file
|
||||||
if not Path(f"{path}").is_file():
|
if not Path(f"{path}").is_file():
|
||||||
print("MMS file not found : aborting", file=sys.stderr)
|
log.error("MMS file not found : aborting")
|
||||||
return False
|
return False
|
||||||
# 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():
|
||||||
print("MMS status file not found : aborting", file=sys.stderr)
|
log.error("MMS status file not found : aborting")
|
||||||
return False
|
return False
|
||||||
status.read_file(open(f"{path}.status"))
|
status.read_file(open(f"{path}.status"))
|
||||||
# Allow only incoming MMS for the time beeing
|
# Allow only incoming MMS for the time beeing
|
||||||
if not (status['info']['state'] == 'downloaded' or
|
if not (status['info']['state'] == 'downloaded' or
|
||||||
status['info']['state'] == 'received'):
|
status['info']['state'] == 'received'):
|
||||||
print("Outgoing MMS : aborting", file=sys.stderr)
|
log.error("Outgoing MMS : aborting")
|
||||||
return False
|
return False
|
||||||
if not (self.force_read or not status.getboolean('info', 'read')):
|
if not (self.force_read or not status.getboolean('info', 'read')):
|
||||||
print("Already converted MMS : aborting", file=sys.stderr)
|
log.error("Already converted MMS : aborting")
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -148,7 +167,7 @@ class MMS2Mail:
|
||||||
"""
|
"""
|
||||||
# Check if the provided file present
|
# Check if the provided file present
|
||||||
if not self.check_mms(path):
|
if not self.check_mms(path):
|
||||||
print("MMS file not convertible.", file=sys.stderr)
|
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)
|
||||||
if not dbus_path:
|
if not dbus_path:
|
||||||
|
@ -156,7 +175,6 @@ class MMS2Mail:
|
||||||
|
|
||||||
mms = MMSMessage.from_file(path)
|
mms = MMSMessage.from_file(path)
|
||||||
|
|
||||||
self.mailer.start()
|
|
||||||
message = Message()
|
message = Message()
|
||||||
|
|
||||||
# Generate Mail Headers
|
# Generate Mail Headers
|
||||||
|
@ -190,7 +208,7 @@ class MMS2Mail:
|
||||||
plain = data_part.data.decode(encoding)
|
plain = data_part.data.decode(encoding)
|
||||||
message.plain += plain + '\n'
|
message.plain += plain + '\n'
|
||||||
continue
|
continue
|
||||||
extension = 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)
|
message.attach(filename + extension, data_part.data)
|
||||||
data_id = data_id + 1
|
data_id = data_id + 1
|
||||||
|
@ -199,16 +217,50 @@ class MMS2Mail:
|
||||||
if self.attach_mms:
|
if self.attach_mms:
|
||||||
message.attach(path, None, None, None, False, path.split('/')[-1])
|
message.attach(path, None, None, None, False, path.split('/')[-1])
|
||||||
|
|
||||||
# Creating an empty file stating the mms as been converted
|
# Write the mail in case of mbox lock retry for 5 minutes
|
||||||
self.mark_mms_read(dbus_path)
|
# 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()
|
||||||
|
self.mailbox.add(mailbox.mboxMessage(str(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
|
||||||
|
self.mark_mms_read(dbus_path)
|
||||||
if self.delete:
|
if self.delete:
|
||||||
self.delete_mms(dbus_path)
|
self.delete_mms(dbus_path)
|
||||||
|
|
||||||
# Write the mail
|
|
||||||
self.mailer.send(message)
|
|
||||||
self.mailer.stop()
|
|
||||||
|
|
||||||
|
|
||||||
class DbusWatcher():
|
class DbusWatcher():
|
||||||
"""Use DBus Signal notification to watch for new MMS."""
|
"""Use DBus Signal notification to watch for new MMS."""
|
||||||
|
@ -232,20 +284,20 @@ class DbusWatcher():
|
||||||
path_keyword="path",
|
path_keyword="path",
|
||||||
interface_keyword="interface")
|
interface_keyword="interface")
|
||||||
mainloop = GLib.MainLoop()
|
mainloop = GLib.MainLoop()
|
||||||
print("Starting DBus watcher mainloop")
|
log.info("Starting DBus watcher mainloop")
|
||||||
try:
|
try:
|
||||||
mainloop.run()
|
mainloop.run()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("Stopping DBus watcher mainloop")
|
log.info("Stopping DBus watcher mainloop")
|
||||||
mainloop.quit()
|
mainloop.quit()
|
||||||
|
|
||||||
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':
|
||||||
print(f"New incoming MMS found ({name.split('/')[-1]})")
|
log.debug(f"New incoming MMS found ({name.split('/')[-1]})")
|
||||||
self.mms2mail.convert(value['Attachments'][0][2], name)
|
self.mms2mail.convert(value['Attachments'][0][2], name)
|
||||||
else:
|
else:
|
||||||
print(f"New outgoing MMS found ({name.split('/')[-1]})")
|
log.debug(f"New outgoing MMS found ({name.split('/')[-1]})")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@ -259,18 +311,27 @@ 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")
|
||||||
|
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 /!\\")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
m = MMS2Mail(args.delete, args.force_read)
|
m = MMS2Mail(args.delete, args.force_read,
|
||||||
|
args.disable_dbus, args.force_unlock)
|
||||||
|
|
||||||
if args.files:
|
if args.files:
|
||||||
for mms_file in args.files:
|
for mms_file in args.files:
|
||||||
m.convert(mms_file)
|
m.convert(mms_file)
|
||||||
elif args.watcher:
|
elif args.watcher:
|
||||||
print("Starting mms2mail in daemon mode")
|
log.info("Starting mms2mail in daemon mode")
|
||||||
w = DbusWatcher(m)
|
w = DbusWatcher(m)
|
||||||
w.run()
|
w.run()
|
||||||
else:
|
else:
|
||||||
|
|
Loading…
Reference in a new issue