def cb_post(url, jdata): log.info(">>>> [callback] POSTing to {}: {}".format(url, jdata)) rq = requests.post(url, headers={'User-Agent': USER_AGENT}, json=json.loads(jdata)) if rq.status_code >= 400: log.warning( ">>>> [callback] POSTing to {} failed with status {}: {}".format( url, rq.status_code, rq.text))
def _process(self, ev): # change name of the file asap, to minimize the probability of racing conditions # when processing across multiple instances of the app if ev.name.startswith("_"): return fn = self.spool_dir + "_" + ev.name try: shutil.move(ev.pathname, fn) except Exception as e: log.warning(">>>> possible MM4 file watcher racing condition: " + str(e)) return # parse the file content to get the from and to addresses try: with open(fn, "rb") as fh: msg = email.message_from_binary_file(fh) dispatch(fh.read(), msg.get('from'), email.utils.getaddresses(msg.get_all('to')) ) except email.errors.MessageParseError as me: log.warning(">>>> MM4 file watcher failed to parse {}: {}" .format(spool_fn, me) ) except Exception as e: log.debug(traceback.format_exc()) log.warning(">>>> MM4 file watcher failed: {}".format(e))
def dispatch(content, sender, receivers, source=None): log.info(">>>> {} inbound on MM4 interface - From: {}, To: {}, length: {}" .format(source or "", sender, receivers, len(content)) ) log.debug(">>>> content: {}{}" .format(content[:4096], ("..." if len(content) > 4096 else "")) ) if len(content) > 4096: log.debug(">>>> ...{}".format(content[-256:])) # get a gateway that can handle the message; preference is to search # by receiver address first, by sending host next, and by sender address last gw = \ cfg['receivers'].get(email.utils.parseaddr(receivers[0])[1]) or \ cfg['peers'].get(source[0]) if source is not None else None or \ cfg['senders'].get(email.utils.parseaddr(sender)[1]) if gw is None: log.warning(">>>> no gateway to process this email") return "555 MAIL FROM/RCPT TO parameters not recognized" mm4rx_id = str(uuid.uuid4()).replace("-", "") # move content as file to be processed fn = repo(TMP_MMS_DIR, mm4rx_id + ".mm4") if cfg['general'].get('smtp_host'): with open(fn, "wb") as fh: fh.write(content) # post a task for the gateway parser q_rx = rq.Queue("QRX-" + gw, connection=rdbq) q_rx.enqueue_call( func='models.gateway.inbound', args=( mm4rx_id + ".mm4", ), job_id=mm4rx_id, meta={ 'retries': MAX_GW_RETRIES }, ttl=30 ) log.info(">>>> message {}, queued for processing by gateway {}".format(mm4rx_id, gw)) return None
if len(sys.argv) < 2: print("To start the MM4 mail utility, use a configuration filename as a command line argument.\n") exit() cfg = configparser.ConfigParser() cfg.read(sys.argv[len(sys.argv) - 1]) TMP_MMS_DIR = cfg['general'].get('tmp_dir', "/tmp/mms/") if not TMP_MMS_DIR.endswith("/"): TMP_MMS_DIR += "/" bind_host = cfg['general'].get('smtp_host', '') bind_port = int(cfg['general'].get('smtp_port', 25)) if bind_host: _1 = MM4SMTPServer(( bind_host, bind_port ), None) log.warning(">>>> MM4 SMTP daemon started, listening on {}:{}".format(bind_host, bind_port)) spool = cfg['general'].get('spool_dir') if spool: wm = pyinotify.WatchManager() h = MaildirEventHandler() h.spool_dir = spool notifier = pyinotify.AsyncNotifier(wm, h) _2 = wm.add_watch(spool, pyinotify.IN_CLOSE_WRITE | pyinotify.IN_MOVED_TO # exclude_filter=pyinotify.ExcludeFilter([ spool + "_*" ]) ) log.warning(">>>> MM4 file daemon started, watching " + spool) asyncore.loop()
def mm7_inbound(gw): log.info("[{}] request received, {} bytes".format( gw, bottle.request.headers['Content-Length'])) raw_content = bottle.request.body.read().decode() log.debug("[{}] request headers: {}".format(gw, [ "{}: {}".format(h, bottle.request.headers.get(h)) for h in bottle.request.headers.keys() ])) log.debug("[{}] >>>> raw content: {}...".format(gw, raw_content[:4096])) if len(raw_content) > 4096: log.debug("[{}] >>>> ... {}".format(gw, raw_content[-256:])) bottle.response.content_type = "text/xml" mime_headers = "Mime-Version: 1.0\nContent-Type: " + bottle.request.headers[ 'Content-Type'] # try parsing lightly, to determine what queue to place this in m = email.message_from_string(mime_headers + "\n\n" + raw_content) try: if m.is_multipart(): parts = m.get_payload() log.debug("[{}] handling as multipart, {} parts".format( gw, len(parts))) env_content = parts[0].get_payload(decode=True).decode() log.debug("[{}] SOAP envelope: {}".format(gw, env_content)) env = ET.fromstring(env_content) else: log.debug("[{}] handling as single part".format(gw)) env = ET.fromstring(m.get_payload(decode=True)) except ET.ParseError as e: log.warning("[{}] Failed to xml-parse the SOAP envelope: {}".format( gw, e)) return bottle.HTTPResponse( status=400, body="Failed to xml-parse the SOAP envelope") # get the transaction tag, treat it as unique ID of the incoming message env_ns = "{" + MM7_NAMESPACE['env'] + "}" transaction_id = None t_header = env.find("./" + env_ns + "Header") or [] for t in t_header: if t.tag.endswith("TransactionID"): transaction_id = t.text.strip() mm7_ns = t.tag.replace("TransactionID", "") break if transaction_id is None: s = "SOAP envelope of received request invalid, at least missing a transaction ID" log.warning("[{}] {}".format(gw, s)) return bottle.HTTPResponse(status=400, body=s) # try to identify the message type mo_meta = \ env.find("./" + env_ns + "Body/" + mm7_ns + "DeliverReq") or \ env.find("./" + env_ns + "Body/" + mm7_ns + "SubmitReq") if mo_meta: # create a shallow message to send back to the MMSC rx = MMSMessage() rx.last_tran_id = transaction_id rx.direction = 1 log.debug("[{}] {} Incoming message {} is an MO".format( gw, rx.id, transaction_id)) rx.save() rx.template.save() # save raw content fn = repo(TMP_MMS_DIR, rx.id + ".mm7") log.debug("[{}] {} saving media as {}".format(gw, rx.id, transaction_id, fn)) with open(fn, "w") as fh: fh.write(parts[1].as_string()) # schedule message for processing q_rx = rq.Queue("QRX-" + gw, connection=rdbq) q_rx.enqueue_call(func='models.gateway.inbound', args=( rx.id + ".mm7", ET.tostring(mo_meta), ), job_id=rx.id, meta={'retries': MAX_GW_RETRIES}, ttl=30) # send MM7 response return models.gateway.MM7Gateway.build_response( "DeliverRsp", transaction_id, rx.id, '1000') m_meta = \ env.find("./" + env_ns + "Body/" + mm7_ns + "DeliveryReportReq") or \ env.find("./" + env_ns + "Body/" + mm7_ns + "ReadReplyReq") if m_meta: m_type = m_meta.tag.replace(mm7_ns, "") m_reply_type = "DeliveryReportRsp" if m_meta.tag.replace( mm7_ns, "") else "ReadReplyRsp" log.debug("[{}] Incoming message is a {}".format(gw, m_type)) provider_msg_id = m_meta.findtext("./" + mm7_ns + "MessageID", "") if provider_msg_id is None: log.warning("[{}] No MessageID tag found in the {}".format( gw, m_meta)) return models.gateway.MM7Gateway.build_response( m_reply_type, transaction_id, "", '4004') # find MT message txid = rdb.get('mmsxref-' + provider_msg_id) if txid is None: log.info( "[{}] Couldnt find reference to original message for MessageID {}" .format(gw, provider_msg_id)) return models.gateway.MM7Gateway.build_response( m_reply_type, transaction_id, provider_msg_id, '2005') tx = MMSMessage(txid) if tx.id is None: log.info( "[{}] {} Couldnt find original message record for MessageID {}" .format(gw, provider_msg_id)) return models.gateway.MM7Gateway.build_response( m_reply_type, transaction_id, provider_msg_id, '2005') # schedule DR for processing q_rx = rq.Queue("QRX-" + gw, connection=rdbq) q_rx.enqueue_call(func='models.gateway.inbound', args=( "", ET.tostring(m_meta), ), job_id=transaction_id, meta={'retries': MAX_GW_RETRIES}, ttl=30) # send MM7 response return models.gateway.MM7Gateway.build_response( m_reply_type, transaction_id, provider_msg_id, '1000') # handling for other MM7 requests (cancel, replace, etc) go here log.warning("[{}] Unknown or unhandled message type".format(gw)) return bottle.HTTPResponse(status=400, body="Unknown or unhandled message type")