first commit

This commit is contained in:
root
2026-03-14 09:42:12 +00:00
commit 0adbd20c2c
10991 changed files with 1646955 additions and 0 deletions

View File

@@ -0,0 +1,322 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import base64
import email
import json
import re
import urllib
import uuid
from email.policy import default as email_policy_default
from email.utils import getaddresses
from functools import partial
import trytond.config as config
from trytond.model import (
ModelSQL, ModelStorage, ModelView, fields, sequence_ordered)
from trytond.pool import Pool
from trytond.pyson import Eval
from trytond.transaction import Transaction
from trytond.url import http_host
if config.getboolean('inbound_email', 'filestore', default=True):
file_id = 'data_id'
store_prefix = config.get('inbound_email', 'store_prefix', default=None)
else:
file_id = store_prefix = None
class Inbox(ModelSQL, ModelView):
__name__ = 'inbound.email.inbox'
name = fields.Char("Name", required=True)
identifier = fields.Char("Identifier", readonly=True)
endpoint = fields.Function(
fields.Char(
"Endpoint",
help="The URL where the emails must be posted."),
'on_change_with_endpoint')
rules = fields.One2Many(
'inbound.email.rule', 'inbox', "Rules",
help="The action of the first matching line is run.")
@classmethod
def __setup__(cls):
super().__setup__()
cls._buttons.update(
new_identifier={
'icon': 'tryton-refresh',
},
)
@fields.depends('identifier')
def on_change_with_endpoint(self, name=None):
if self.identifier:
url_part = {
'identifier': self.identifier,
'database_name': Transaction().database.name,
}
return http_host() + (
urllib.parse.quote(
'/%(database_name)s/inbound_email/inbox/%(identifier)s'
% url_part))
@classmethod
@ModelView.button
def new_identifier(cls, inboxes):
for inbox in inboxes:
if inbox.identifier:
inbox.identifier = None
else:
inbox.identifier = uuid.uuid4().hex
cls.save(inboxes)
def process(self, email_):
assert email_.inbox == self
for rule in self.rules:
if rule.match(email_.as_dict()):
email_.rule = rule
rule.run(email_)
return
def _email_text(message, type_='plain'):
if message.get_content_maintype() != 'multipart':
return message.get_payload()
for part in message.walk():
if part.get_content_type() == f'text/{type_}':
return part.get_payload()
def _email_attachments(message):
if message.get_content_maintype() != 'multipart':
return
for i, part in enumerate(message.walk()):
if part.get_content_maintype() == 'multipart':
continue
if 'attachment' not in part.get('Content-Disposition', '').lower():
continue
filename = part.get_filename()
yield {
'filename': filename,
'type': part.get_content_type(),
'data': part.get_payload(decode=True),
}
class Email(ModelSQL, ModelView):
__name__ = 'inbound.email'
inbox = fields.Many2One(
'inbound.email.inbox', "Inbox",
required=True, readonly=True, ondelete='CASCADE')
data = fields.Binary(
"Data", file_id=file_id, store_prefix=store_prefix,
required=True, readonly=True)
data_id = fields.Char("Data ID", readonly=True)
data_type = fields.Selection([
('mailchimp', "Mailchimp"),
('mailpace', "MailPace"),
('postmark', "Postmark"),
('raw', "Raw"),
('sendgrid', "SendGrid"),
], "Data Type", required=True, readonly=True, translate=False)
rule = fields.Many2One('inbound.email.rule', "Rule", readonly=True)
result = fields.Reference("Result", selection='get_models', readonly=True)
@classmethod
def __setup__(cls):
super().__setup__()
cls._order = [('id', 'DESC')]
cls._buttons.update(
process={
'readonly': Eval('rule'),
'depends': ['rule'],
},
)
@classmethod
def get_models(cls):
pool = Pool()
Model = pool.get('ir.model')
return [(None, "")] + Model.get_name_items((ModelStorage, ModelView))
@classmethod
def from_webhook(cls, inbox, data, data_type):
emails = []
if data_type in {'raw', 'mailpace', 'sendgrid', 'postmark'}:
emails.append(cls(inbox=inbox, data=data, data_type=data_type))
elif data_type == 'mailchimp':
payload = json.loads(data)
for event in payload['mandrill_events']:
if event['event'] == 'inbound':
emails.append(cls(
inbox=inbox,
data=json.dumps(event).encode('utf-8'),
data_type=data_type))
return emails
@classmethod
@ModelView.button
def process(cls, emails):
for email_ in emails:
if not email_.rule:
email_.inbox.process(email_)
cls.save(emails)
def as_dict(self):
value = {}
if self.data_type == 'raw':
value.update(self._as_dict(self.data))
elif self.data_type == 'mailchimp':
event = json.loads(self.data)
value.update(self._as_dict(event['raw_msg']))
elif self.data_type == 'mailpace':
payload = json.loads(self.data)
value.update(self._as_dict(payload['raw']))
elif self.data_type == 'postmark':
payload = json.loads(self.data)
value.update(self._as_dict_postmark(payload))
elif self.data_type == 'sendgrid':
payload = json.loads(self.data)
value.update(self._as_dict(payload['email']))
return value
def _as_dict(self, raw):
value = {}
if isinstance(raw, str):
message = email.message_from_string(
raw, policy=email_policy_default)
else:
message = email.message_from_bytes(
raw, policy=email_policy_default)
if 'From' in message:
value['from'] = getaddresses([message.get('From')])[0][1]
for key in ['To', 'Cc', 'Bcc']:
if key in message:
value[key.lower()] = [
a for _, a in getaddresses(message.get_all(key))]
if 'Subject' in message:
value['subject'] = message['Subject']
text = _email_text(message)
if text is not None:
value['text'] = text
html = _email_text(message, 'html')
if html is not None:
value['html'] = html
value['attachments'] = list(_email_attachments(message))
value['headers'] = dict(message.items())
return value
def _as_dict_postmark(self, payload):
value = {}
if 'FromFull' in payload:
value['from'] = payload['FromFull']['Email']
for key in ['To', 'Cc', 'Bcc']:
if f'{key}Full' in payload:
value[key.lower()] = [
a['Email'] for a in payload[f'{key}Full']]
if 'Subject' in payload:
value['subject'] = payload['Subject']
if 'TextBody' in payload:
value['text'] = payload['TextBody']
if 'HtmlBody' in payload:
value['html'] = payload['HtmlBody']
if 'Attachments' in payload:
value['attachments'] = [{
'filename': a['Name'],
'type': a['ContentType'],
'data': base64.b64decode(a['Content']),
} for a in payload['Attachments']]
if 'Headers' in payload:
value['headers'] = {
h['Name']: h['Value'] for h in payload['Headers']}
return value
class Rule(sequence_ordered(), ModelSQL, ModelView):
__name__ = 'inbound.email.rule'
inbox = fields.Many2One(
'inbound.email.inbox', "Inbox", required=True, ondelete='CASCADE')
origin = fields.Char(
"Origin",
help="A regular expression to match the sender email address.")
destination = fields.Char(
"Destination",
help="A regular expression to match any receiver email addresses.")
subject = fields.Char(
"Subject",
help="A regular expression to match the subject.")
attachment_name = fields.Char(
"Attachment Name",
help="A regular expression to match any attachment name.")
headers = fields.One2Many('inbound.email.rule.header', 'rule', "Headers")
action = fields.Selection([
(None, ""),
], "Action")
@classmethod
def __setup__(cls):
super().__setup__()
cls.__access__.add('inbox')
cls._order.insert(0, ('inbox.id', 'DESC'))
def match(self, email_):
flags = re.IGNORECASE
search = partial(re.search, flags=flags)
compile_ = partial(re.compile, flags=flags)
if self.origin:
if not search(self.origin, email_.get('from', '')):
return False
if self.destination:
destinations = [
*email_.get('to', []),
*email_.get('cc', []),
*email_.get('bcc', []),
]
pattern = compile_(self.destination)
if not any(pattern.search(d) for d in destinations):
return False
if self.subject:
if not search(self.subject, email_.get('subject', '')):
return False
if self.attachment_name:
pattern = compile_(self.attachment_name)
if not any(
pattern.search(a.get('filename', ''))
for a in email_.get('attachments', [])):
return False
if self.headers:
for header in self.headers:
if not search(
header.value,
email_.get('headers', {}).get(header.name, '')):
return False
return True
def run(self, email_):
pool = Pool()
if self.action:
model, method = self.action.split('|')
Model = pool.get(model)
email_.result = getattr(Model, method)(email_, self)
class RuleHeader(ModelSQL, ModelView):
__name__ = 'inbound.email.rule.header'
rule = fields.Many2One(
'inbound.email.rule', "Rule", required=True, ondelete='CASCADE')
name = fields.Char(
"Name", required=True,
help="The name of the header.")
value = fields.Char(
"Value",
help="A regular expression to match the header value.")
@classmethod
def __setup__(cls):
super().__setup__()
cls.__access__.add('rule')