mirror of
https://bitbucket.org/presik/trytonpsk-smtp.git
synced 2023-12-14 05:22:56 +01:00
confirguration smtp client
This commit is contained in:
parent
b665e950ea
commit
a9664f93dc
7 changed files with 203 additions and 96 deletions
|
@ -3,10 +3,11 @@
|
||||||
# the full copyright notices and license terms.
|
# the full copyright notices and license terms.
|
||||||
from trytond.pool import Pool
|
from trytond.pool import Pool
|
||||||
from . import smtp
|
from . import smtp
|
||||||
|
from . import email_
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
Pool.register(
|
Pool.register(
|
||||||
smtp.SmtpServer,
|
smtp.SmtpConnectionMail,
|
||||||
smtp.SmtpServerModel,
|
email_.Email,
|
||||||
module='smtp', type_='model')
|
module='smtp', type_='model')
|
||||||
|
|
141
email_.py
Normal file
141
email_.py
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import heapq
|
||||||
|
import mimetypes
|
||||||
|
import re
|
||||||
|
from email.encoders import encode_base64
|
||||||
|
from email.header import Header
|
||||||
|
from email.mime.application import MIMEApplication
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.nonmultipart import MIMENonMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.utils import formataddr, getaddresses
|
||||||
|
|
||||||
|
try:
|
||||||
|
import html2text
|
||||||
|
except ImportError:
|
||||||
|
html2text = None
|
||||||
|
from genshi.template import TextTemplate
|
||||||
|
|
||||||
|
# from trytond.i18n import gettext
|
||||||
|
from trytond.config import config
|
||||||
|
from trytond.pool import Pool, PoolMeta
|
||||||
|
from trytond.report import Report
|
||||||
|
from trytond.ir.email_ import HTML_EMAIL, _get_emails
|
||||||
|
from trytond.sendmail import sendmail_transactional, SMTPDataManager
|
||||||
|
from trytond.transaction import Transaction
|
||||||
|
from .sendmail import _SMTPDataManager
|
||||||
|
|
||||||
|
class Email(metaclass=PoolMeta):
|
||||||
|
"Email"
|
||||||
|
__name__ = 'ir.email'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def send(cls, to='', cc='', bcc='', subject='', body='',
|
||||||
|
files=None, record=None, reports=None, attachments=None):
|
||||||
|
pool = Pool()
|
||||||
|
User = pool.get('res.user')
|
||||||
|
ActionReport = pool.get('ir.action.report')
|
||||||
|
Attachment = pool.get('ir.attachment')
|
||||||
|
transaction = Transaction()
|
||||||
|
user = User(transaction.user)
|
||||||
|
|
||||||
|
SmtpConnection = pool.get('smtp.connection.mail')
|
||||||
|
smtps = SmtpConnection.search([('user', '=', user.id)])
|
||||||
|
uri = None
|
||||||
|
user_email = None
|
||||||
|
if smtps:
|
||||||
|
uri = smtps[0].get_uri()
|
||||||
|
user_email = smtps[0].smtp_email
|
||||||
|
|
||||||
|
Model = pool.get(record[0])
|
||||||
|
record = Model(record[1])
|
||||||
|
|
||||||
|
body_html = HTML_EMAIL % {
|
||||||
|
'subject': subject,
|
||||||
|
'body': body,
|
||||||
|
'signature': user.signature or '',
|
||||||
|
}
|
||||||
|
content = MIMEMultipart('alternative')
|
||||||
|
if html2text:
|
||||||
|
body_text = HTML_EMAIL % {
|
||||||
|
'subject': subject,
|
||||||
|
'body': body,
|
||||||
|
'signature': '',
|
||||||
|
}
|
||||||
|
converter = html2text.HTML2Text()
|
||||||
|
body_text = converter.handle(body_text)
|
||||||
|
if user.signature:
|
||||||
|
body_text += '\n-- \n' + converter.handle(user.signature)
|
||||||
|
part = MIMEText(body_text, 'plain', _charset='utf-8')
|
||||||
|
content.attach(part)
|
||||||
|
part = MIMEText(body_html, 'html', _charset='utf-8')
|
||||||
|
content.attach(part)
|
||||||
|
if files or reports or attachments:
|
||||||
|
msg = MIMEMultipart('mixed')
|
||||||
|
msg.attach(content)
|
||||||
|
if files is None:
|
||||||
|
files = []
|
||||||
|
else:
|
||||||
|
files = list(files)
|
||||||
|
|
||||||
|
for report_id in (reports or []):
|
||||||
|
report = ActionReport(report_id)
|
||||||
|
Report = pool.get(report.report_name, type='report')
|
||||||
|
ext, content, _, title = Report.execute(
|
||||||
|
[record.id], {
|
||||||
|
'action_id': report.id,
|
||||||
|
})
|
||||||
|
name = '%s.%s' % (title, ext)
|
||||||
|
if isinstance(content, str):
|
||||||
|
content = content.encode('utf-8')
|
||||||
|
files.append((name, content))
|
||||||
|
if attachments:
|
||||||
|
files += [
|
||||||
|
(a.name, a.data) for a in Attachment.browse(attachments)]
|
||||||
|
for name, data in files:
|
||||||
|
mimetype, _ = mimetypes.guess_type(name)
|
||||||
|
if mimetype:
|
||||||
|
attachment = MIMENonMultipart(*mimetype.split('/'))
|
||||||
|
attachment.set_payload(data)
|
||||||
|
encode_base64(attachment)
|
||||||
|
else:
|
||||||
|
attachment = MIMEApplication(data)
|
||||||
|
attachment.add_header(
|
||||||
|
'Content-Disposition', 'attachment',
|
||||||
|
filename=('utf-8', '', name))
|
||||||
|
msg.attach(attachment)
|
||||||
|
else:
|
||||||
|
msg = content
|
||||||
|
msg['From'] = from_ = user_email
|
||||||
|
if user.email:
|
||||||
|
if user.name:
|
||||||
|
user_email = formataddr((user.name, user.email))
|
||||||
|
else:
|
||||||
|
user_email = user.email
|
||||||
|
msg['Behalf-Of'] = user_email
|
||||||
|
msg['Reply-To'] = user_email
|
||||||
|
msg['To'] = ', '.join(formataddr(a) for a in getaddresses([to]))
|
||||||
|
msg['Cc'] = ', '.join(formataddr(a) for a in getaddresses([cc]))
|
||||||
|
msg['Subject'] = Header(subject, 'utf-8')
|
||||||
|
|
||||||
|
to_addrs = list(filter(None, map(
|
||||||
|
str.strip,
|
||||||
|
_get_emails(to) + _get_emails(cc) + _get_emails(bcc))))
|
||||||
|
sendmail_transactional(
|
||||||
|
from_, to_addrs, msg, datamanager=SMTPDataManager(uri=uri, strict=True))
|
||||||
|
|
||||||
|
email = cls(
|
||||||
|
recipients=to,
|
||||||
|
recipients_secondary=cc,
|
||||||
|
recipients_hidden=bcc,
|
||||||
|
addresses=[{'address': a} for a in to_addrs],
|
||||||
|
subject=subject,
|
||||||
|
body=body,
|
||||||
|
resource=record)
|
||||||
|
email.save()
|
||||||
|
with Transaction().set_context(_check_access=False):
|
||||||
|
attachments_ = []
|
||||||
|
for name, data in files:
|
||||||
|
attachments_.append(
|
||||||
|
Attachment(resource=email, name=name, data=data))
|
||||||
|
Attachment.save(attachments_)
|
||||||
|
return email
|
22
setup.py
22
setup.py
|
@ -90,25 +90,15 @@ setup(name=name,
|
||||||
'Intended Audience :: Financial and Insurance Industry',
|
'Intended Audience :: Financial and Insurance Industry',
|
||||||
'Intended Audience :: Legal Industry',
|
'Intended Audience :: Legal Industry',
|
||||||
'License :: OSI Approved :: GNU General Public License (GPL)',
|
'License :: OSI Approved :: GNU General Public License (GPL)',
|
||||||
'Natural Language :: Bulgarian',
|
|
||||||
'Natural Language :: Catalan',
|
|
||||||
'Natural Language :: Chinese (Simplified)',
|
|
||||||
'Natural Language :: Czech',
|
'Natural Language :: Czech',
|
||||||
'Natural Language :: Dutch',
|
'Natural Language :: Dutch',
|
||||||
'Natural Language :: English',
|
'Natural Language :: English',
|
||||||
'Natural Language :: French',
|
|
||||||
'Natural Language :: German',
|
|
||||||
'Natural Language :: Hungarian',
|
|
||||||
'Natural Language :: Italian',
|
|
||||||
'Natural Language :: Portuguese (Brazilian)',
|
|
||||||
'Natural Language :: Russian',
|
|
||||||
'Natural Language :: Slovenian',
|
|
||||||
'Natural Language :: Spanish',
|
'Natural Language :: Spanish',
|
||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
'Programming Language :: Python :: 2.7',
|
'Programming Language :: Python :: 3.7',
|
||||||
'Programming Language :: Python :: 3.3',
|
'Programming Language :: Python :: 3.8',
|
||||||
'Programming Language :: Python :: 3.4',
|
'Programming Language :: Python :: 3.9',
|
||||||
'Programming Language :: Python :: 3.5',
|
'Programming Language :: Python :: 3.10',
|
||||||
'Programming Language :: Python :: Implementation :: CPython',
|
'Programming Language :: Python :: Implementation :: CPython',
|
||||||
'Programming Language :: Python :: Implementation :: PyPy',
|
'Programming Language :: Python :: Implementation :: PyPy',
|
||||||
'Topic :: Office/Business',
|
'Topic :: Office/Business',
|
||||||
|
@ -125,8 +115,4 @@ setup(name=name,
|
||||||
test_suite='tests',
|
test_suite='tests',
|
||||||
test_loader='trytond.test_loader:Loader',
|
test_loader='trytond.test_loader:Loader',
|
||||||
tests_require=tests_require,
|
tests_require=tests_require,
|
||||||
use_2to3=True,
|
|
||||||
convert_2to3_doctests=[
|
|
||||||
'tests/scenario.rst',
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
102
smtp.py
102
smtp.py
|
@ -8,9 +8,9 @@ from trytond.pool import Pool
|
||||||
from trytond.pyson import Eval
|
from trytond.pyson import Eval
|
||||||
|
|
||||||
|
|
||||||
class SmtpServer(ModelSQL, ModelView):
|
class SmtpConnectionMail(ModelSQL, ModelView):
|
||||||
'SMTP Servers'
|
'SMTP Conection Mail'
|
||||||
__name__ = 'smtp.server'
|
__name__ = 'smtp.connection.mail'
|
||||||
name = fields.Char('Name', required=True)
|
name = fields.Char('Name', required=True)
|
||||||
smtp_server = fields.Char('Server', required=True,
|
smtp_server = fields.Char('Server', required=True,
|
||||||
states={
|
states={
|
||||||
|
@ -28,18 +28,10 @@ class SmtpServer(ModelSQL, ModelView):
|
||||||
states={
|
states={
|
||||||
'readonly': (Eval('state') != 'draft'),
|
'readonly': (Eval('state') != 'draft'),
|
||||||
}, depends=['state'])
|
}, depends=['state'])
|
||||||
smtp_user = fields.Char('User',
|
|
||||||
states={
|
|
||||||
'readonly': (Eval('state') != 'draft'),
|
|
||||||
}, depends=['state'])
|
|
||||||
smtp_password = fields.Char('Password',
|
smtp_password = fields.Char('Password',
|
||||||
states={
|
states={
|
||||||
'readonly': (Eval('state') != 'draft'),
|
'readonly': (Eval('state') != 'draft'),
|
||||||
}, depends=['state'])
|
}, depends=['state'])
|
||||||
smtp_use_email = fields.Boolean('Use email',
|
|
||||||
states={
|
|
||||||
'readonly': (Eval('state') != 'draft'),
|
|
||||||
}, depends=['state'], help='Force to send emails using this email')
|
|
||||||
smtp_email = fields.Char('Email', required=True,
|
smtp_email = fields.Char('Email', required=True,
|
||||||
states={
|
states={
|
||||||
'readonly': (Eval('state') != 'draft'),
|
'readonly': (Eval('state') != 'draft'),
|
||||||
|
@ -49,24 +41,11 @@ class SmtpServer(ModelSQL, ModelView):
|
||||||
('draft', 'Draft'),
|
('draft', 'Draft'),
|
||||||
('done', 'Done'),
|
('done', 'Done'),
|
||||||
], 'State', readonly=True, required=True)
|
], 'State', readonly=True, required=True)
|
||||||
default = fields.Boolean('Default')
|
user = fields.Many2One('res.user', 'User')
|
||||||
models = fields.Many2Many('smtp.server-ir.model',
|
|
||||||
'server', 'model', 'Models',
|
|
||||||
states={
|
|
||||||
'readonly': Eval('state').in_(['done']),
|
|
||||||
},
|
|
||||||
depends=['state'])
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __setup__(cls):
|
def __setup__(cls):
|
||||||
super(SmtpServer, cls).__setup__()
|
super(SmtpConnectionMail, cls).__setup__()
|
||||||
cls._error_messages.update({
|
|
||||||
'smtp_successful': 'SMTP Test Connection Was Successful',
|
|
||||||
'smtp_test_details': 'SMTP Test Connection Details:\n%s',
|
|
||||||
'smtp_error': 'SMTP Test Connection Failed.',
|
|
||||||
'server_model_not_found': (
|
|
||||||
'There are not SMTP server related at model %s'),
|
|
||||||
})
|
|
||||||
cls._buttons.update({
|
cls._buttons.update({
|
||||||
'get_smtp_test': {},
|
'get_smtp_test': {},
|
||||||
'draft': {
|
'draft': {
|
||||||
|
@ -81,9 +60,6 @@ class SmtpServer(ModelSQL, ModelView):
|
||||||
def check_xml_record(cls, records, values):
|
def check_xml_record(cls, records, values):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def default_default():
|
|
||||||
return True
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def default_smtp_ssl():
|
def default_smtp_ssl():
|
||||||
|
@ -97,6 +73,15 @@ class SmtpServer(ModelSQL, ModelView):
|
||||||
def default_state():
|
def default_state():
|
||||||
return 'draft'
|
return 'draft'
|
||||||
|
|
||||||
|
def get_uri(self):
|
||||||
|
option = 'smtp'
|
||||||
|
if self.smtp_tls:
|
||||||
|
option = 'smtp+tls'
|
||||||
|
elif self.smtp_ssl:
|
||||||
|
option = 'smtps'
|
||||||
|
# smtps://cuenta@dominio.com:miclave@mail.dominio.com:465
|
||||||
|
return option + '://' + self.smtp_email + ':' + self.smtp_password + '@' + self.smtp_server + ':' + str(self.smtp_port)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ModelView.button
|
@ModelView.button
|
||||||
def draft(cls, servers):
|
def draft(cls, servers):
|
||||||
|
@ -116,13 +101,14 @@ class SmtpServer(ModelSQL, ModelView):
|
||||||
def get_smtp_test(cls, servers):
|
def get_smtp_test(cls, servers):
|
||||||
"""Checks SMTP credentials and confirms if outgoing connection works"""
|
"""Checks SMTP credentials and confirms if outgoing connection works"""
|
||||||
for server in servers:
|
for server in servers:
|
||||||
try:
|
# try:
|
||||||
server.get_smtp_server()
|
server = server.get_smtp_server()
|
||||||
except Exception, message:
|
print(server, 'server')
|
||||||
cls.raise_user_error('smtp_test_details', message)
|
# except e:
|
||||||
except:
|
# cls.raise_user_error('smtp_test_details', message)
|
||||||
cls.raise_user_error('smtp_error')
|
# except:
|
||||||
cls.raise_user_error('smtp_successful')
|
# cls.raise_user_error('smtp_error')
|
||||||
|
# cls.raise_user_error('smtp_successful')
|
||||||
|
|
||||||
def get_smtp_server(self):
|
def get_smtp_server(self):
|
||||||
"""
|
"""
|
||||||
|
@ -144,28 +130,28 @@ class SmtpServer(ModelSQL, ModelView):
|
||||||
|
|
||||||
return smtp_server
|
return smtp_server
|
||||||
|
|
||||||
@classmethod
|
# @classmethod
|
||||||
def get_smtp_server_from_model(self, model):
|
# def get_smtp_server_from_model(self, model):
|
||||||
"""
|
# """
|
||||||
Return Server from Models
|
# Return Server from Models
|
||||||
:param model: str Model name
|
# :param model: str Model name
|
||||||
return object server
|
# return object server
|
||||||
"""
|
# """
|
||||||
model = Pool().get('ir.model').search([('model', '=', model)])[0]
|
# model = Pool().get('ir.model').search([('model', '=', model)])[0]
|
||||||
servers = Pool().get('smtp.server-ir.model').search([
|
# servers = Pool().get('smtp.server-ir.model').search([
|
||||||
('model', '=', model),
|
# ('model', '=', model),
|
||||||
], limit=1)
|
# ], limit=1)
|
||||||
if not servers:
|
# if not servers:
|
||||||
self.raise_user_error('server_model_not_found', model.name)
|
# self.raise_user_error('server_model_not_found', model.name)
|
||||||
return servers[0].server
|
# return servers[0].server
|
||||||
|
|
||||||
|
|
||||||
class SmtpServerModel(ModelSQL):
|
# class SmtpServerModel(ModelSQL):
|
||||||
'SMTP Server - Model'
|
# 'SMTP Server - Model'
|
||||||
__name__ = 'smtp.server-ir.model'
|
# __name__ = 'smtp.server-ir.model'
|
||||||
_table = 'smtp_server_ir_model'
|
# _table = 'smtp_server_ir_model'
|
||||||
|
|
||||||
server = fields.Many2One('smtp.server', 'Server', ondelete='CASCADE',
|
# server = fields.Many2One('smtp.server', 'Server', ondelete='CASCADE',
|
||||||
select=True, required=True)
|
# select=True, required=True)
|
||||||
model = fields.Many2One('ir.model', 'Model', ondelete='RESTRICT',
|
# model = fields.Many2One('ir.model', 'Model', ondelete='RESTRICT',
|
||||||
select=True, required=True)
|
# select=True, required=True)
|
||||||
|
|
16
smtp.xml
16
smtp.xml
|
@ -18,20 +18,20 @@ The COPYRIGHT file at the top level of this repository contains the full copyrig
|
||||||
|
|
||||||
<!-- smtp.server -->
|
<!-- smtp.server -->
|
||||||
<record model="ir.ui.view" id="server_view_form">
|
<record model="ir.ui.view" id="server_view_form">
|
||||||
<field name="model">smtp.server</field>
|
<field name="model">smtp.connection.mail</field>
|
||||||
<field name="type">form</field>
|
<field name="type">form</field>
|
||||||
<field name="name">smtp_server_form</field>
|
<field name="name">smtp_connection_mail_form</field>
|
||||||
</record>
|
</record>
|
||||||
<record model="ir.ui.view" id="server_view_tree">
|
<record model="ir.ui.view" id="server_view_tree">
|
||||||
<field name="model">smtp.server</field>
|
<field name="model">smtp.connection.mail</field>
|
||||||
<field name="type">tree</field>
|
<field name="type">tree</field>
|
||||||
<field name="name">smtp_server_tree</field>
|
<field name="name">smtp_connection_mail_tree</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- Actions / Menu -->
|
<!-- Actions / Menu -->
|
||||||
<record model="ir.action.act_window" id="act_server_form">
|
<record model="ir.action.act_window" id="act_server_form">
|
||||||
<field name="name">Servers</field>
|
<field name="name">Smtp Connection Users</field>
|
||||||
<field name="res_model">smtp.server</field>
|
<field name="res_model">smtp.connection.mail</field>
|
||||||
</record>
|
</record>
|
||||||
<record model="ir.action.act_window.view" id="act_server_form_view1">
|
<record model="ir.action.act_window.view" id="act_server_form_view1">
|
||||||
<field name="sequence" eval="10"/>
|
<field name="sequence" eval="10"/>
|
||||||
|
@ -67,7 +67,7 @@ The COPYRIGHT file at the top level of this repository contains the full copyrig
|
||||||
id="menu_server_form"/>
|
id="menu_server_form"/>
|
||||||
|
|
||||||
<!-- Access -->
|
<!-- Access -->
|
||||||
<record model="ir.model.access" id="access_smtp_server">
|
<!-- <record model="ir.model.access" id="access_smtp_server">
|
||||||
<field name="model" search="[('model', '=', 'smtp.server')]"/>
|
<field name="model" search="[('model', '=', 'smtp.server')]"/>
|
||||||
<field name="perm_read" eval="True"/>
|
<field name="perm_read" eval="True"/>
|
||||||
<field name="perm_write" eval="False"/>
|
<field name="perm_write" eval="False"/>
|
||||||
|
@ -104,6 +104,6 @@ The COPYRIGHT file at the top level of this repository contains the full copyrig
|
||||||
<field name="perm_write" eval="True"/>
|
<field name="perm_write" eval="True"/>
|
||||||
<field name="perm_create" eval="True"/>
|
<field name="perm_create" eval="True"/>
|
||||||
<field name="perm_delete" eval="True"/>
|
<field name="perm_delete" eval="True"/>
|
||||||
</record>
|
</record> -->
|
||||||
</data>
|
</data>
|
||||||
</tryton>
|
</tryton>
|
||||||
|
|
|
@ -4,8 +4,8 @@ The COPYRIGHT file at the top level of this repository contains the full copyrig
|
||||||
<form>
|
<form>
|
||||||
<label name="name"/>
|
<label name="name"/>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<label name="default"/>
|
<label name="user"/>
|
||||||
<field name="default"/>
|
<field name="user"/>
|
||||||
<notebook colspan="4">
|
<notebook colspan="4">
|
||||||
<page string="Server" id="server">
|
<page string="Server" id="server">
|
||||||
<label name="smtp_server"/>
|
<label name="smtp_server"/>
|
||||||
|
@ -16,12 +16,8 @@ The COPYRIGHT file at the top level of this repository contains the full copyrig
|
||||||
<field name="smtp_ssl"/>
|
<field name="smtp_ssl"/>
|
||||||
<label name="smtp_tls"/>
|
<label name="smtp_tls"/>
|
||||||
<field name="smtp_tls"/>
|
<field name="smtp_tls"/>
|
||||||
<label name="smtp_user"/>
|
|
||||||
<field name="smtp_user"/>
|
|
||||||
<label name="smtp_password"/>
|
<label name="smtp_password"/>
|
||||||
<field name="smtp_password" widget="password"/>
|
<field name="smtp_password" widget="password"/>
|
||||||
<label name="smtp_use_email"/>
|
|
||||||
<field name="smtp_use_email"/>
|
|
||||||
<label name="smtp_email"/>
|
<label name="smtp_email"/>
|
||||||
<field name="smtp_email"/>
|
<field name="smtp_email"/>
|
||||||
<group col="6" colspan="6" id="server_buttons">
|
<group col="6" colspan="6" id="server_buttons">
|
||||||
|
@ -32,8 +28,5 @@ The COPYRIGHT file at the top level of this repository contains the full copyrig
|
||||||
<button name="get_smtp_test" string="Test Connection" icon="tryton-ok"/>
|
<button name="get_smtp_test" string="Test Connection" icon="tryton-ok"/>
|
||||||
</group>
|
</group>
|
||||||
</page>
|
</page>
|
||||||
<page string="Models" id="model">
|
|
||||||
<field name="models"/>
|
|
||||||
</page>
|
|
||||||
</notebook>
|
</notebook>
|
||||||
</form>
|
</form>
|
|
@ -2,8 +2,8 @@
|
||||||
<!-- This file is part smtp module for Tryton.
|
<!-- This file is part smtp module for Tryton.
|
||||||
The COPYRIGHT file at the top level of this repository contains the full copyright notices and license terms. -->
|
The COPYRIGHT file at the top level of this repository contains the full copyright notices and license terms. -->
|
||||||
<tree>
|
<tree>
|
||||||
|
<field name="user"/>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="smtp_server"/>
|
<field name="smtp_server"/>
|
||||||
<field name="smtp_user"/>
|
|
||||||
<field name="state"/>
|
<field name="state"/>
|
||||||
</tree>
|
</tree>
|
Loading…
Reference in a new issue