From 093418ec59b15f2af2253be59c25efd974342c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20V=C3=A9zina?= Date: Wed, 28 Jun 2023 21:21:29 -0400 Subject: [PATCH] all models, need to check everything --- bemade_mailcow_blacklist/__init__.py | 3 + bemade_mailcow_blacklist/__manifest__.py | 38 +++++ bemade_mailcow_blacklist/demo/demo.xml | 30 ++++ bemade_mailcow_blacklist/models/__init__.py | 9 ++ bemade_mailcow_blacklist/models/mail_alias.py | 20 +++ bemade_mailcow_blacklist/models/mailcow.py | 48 +++++++ .../models/mailcow_alias.py | 97 +++++++++++++ .../models/mailcow_blacklist.py | 69 +++++++++ .../models/mailcow_mailbox.py | 134 ++++++++++++++++++ .../models/res_config_settings.py | 15 ++ bemade_mailcow_blacklist/models/res_users.py | 14 ++ .../security/ir.model.access.csv | 7 + .../views/mailcow_alias_views.xml | 58 ++++++++ .../views/mailcow_blacklist_views.xml | 52 +++++++ .../views/mailcow_mailbox_views.xml | 67 +++++++++ .../views/res_config_settings_views.xml | 39 +++++ .../views/res_users_views.xml | 20 +++ bemade_mailcow_blacklist/views/templates.xml | 24 ++++ bemade_mailcow_blacklist/views/views.xml | 60 ++++++++ 19 files changed, 804 insertions(+) create mode 100644 bemade_mailcow_blacklist/__init__.py create mode 100644 bemade_mailcow_blacklist/__manifest__.py create mode 100644 bemade_mailcow_blacklist/demo/demo.xml create mode 100644 bemade_mailcow_blacklist/models/__init__.py create mode 100644 bemade_mailcow_blacklist/models/mail_alias.py create mode 100644 bemade_mailcow_blacklist/models/mailcow.py create mode 100644 bemade_mailcow_blacklist/models/mailcow_alias.py create mode 100644 bemade_mailcow_blacklist/models/mailcow_blacklist.py create mode 100644 bemade_mailcow_blacklist/models/mailcow_mailbox.py create mode 100644 bemade_mailcow_blacklist/models/res_config_settings.py create mode 100644 bemade_mailcow_blacklist/models/res_users.py create mode 100644 bemade_mailcow_blacklist/security/ir.model.access.csv create mode 100644 bemade_mailcow_blacklist/views/mailcow_alias_views.xml create mode 100644 bemade_mailcow_blacklist/views/mailcow_blacklist_views.xml create mode 100644 bemade_mailcow_blacklist/views/mailcow_mailbox_views.xml create mode 100644 bemade_mailcow_blacklist/views/res_config_settings_views.xml create mode 100644 bemade_mailcow_blacklist/views/res_users_views.xml create mode 100644 bemade_mailcow_blacklist/views/templates.xml create mode 100644 bemade_mailcow_blacklist/views/views.xml diff --git a/bemade_mailcow_blacklist/__init__.py b/bemade_mailcow_blacklist/__init__.py new file mode 100644 index 0000000..cde864b --- /dev/null +++ b/bemade_mailcow_blacklist/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models diff --git a/bemade_mailcow_blacklist/__manifest__.py b/bemade_mailcow_blacklist/__manifest__.py new file mode 100644 index 0000000..e77fa93 --- /dev/null +++ b/bemade_mailcow_blacklist/__manifest__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Mailcow Integration', + 'version': '1.0.0', + 'category': 'Extra Tools', + 'summary': 'Module for integrating Mailcow email server with Odoo.', + 'description': """ +Mailcow Integration +=================== + +This module integrates the Mailcow email server with Odoo, providing a seamless email communication solution for your Odoo instance. It allows for syncing of mailboxes and email aliases from Mailcow to Odoo and vice versa. + +Main Features: +-------------- +* Synchronize Mailcow mailboxes with Odoo users. +* Synchronize Mailcow email aliases with Odoo. +* Configuration of Mailcow API credentials in Odoo settings. +* Automatically create and manage mailboxes and aliases in Mailcow when they are created in Odoo. +""", + 'sequence': 10, + 'license': 'OPL-1', + 'author': 'BeMade', + 'website': 'https://www.bemade.org', + 'depends': ['mail', 'bemade_user_password_bundle'], + 'data': [ + 'security/ir.model.access.csv', + 'views/res_config_settings_views.xml', + 'views/mailcow_mailbox_views.xml', + 'views/mailcow_alias_views.xml', + 'views/mailcow_blacklist_views.xml', + 'views/res_users_views.xml', + + ], + 'demo': [], + 'installable': True, + 'application': False, + 'auto_install': False, +} diff --git a/bemade_mailcow_blacklist/demo/demo.xml b/bemade_mailcow_blacklist/demo/demo.xml new file mode 100644 index 0000000..60b6c36 --- /dev/null +++ b/bemade_mailcow_blacklist/demo/demo.xml @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/bemade_mailcow_blacklist/models/__init__.py b/bemade_mailcow_blacklist/models/__init__.py new file mode 100644 index 0000000..0d900ad --- /dev/null +++ b/bemade_mailcow_blacklist/models/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +from . import mail_alias +from . import mailcow +from . import mailcow_alias +from . import mailcow_blacklist +from . import mailcow_mailbox +from . import res_config_settings +from . import res_users diff --git a/bemade_mailcow_blacklist/models/mail_alias.py b/bemade_mailcow_blacklist/models/mail_alias.py new file mode 100644 index 0000000..bdb7105 --- /dev/null +++ b/bemade_mailcow_blacklist/models/mail_alias.py @@ -0,0 +1,20 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + +class MailAlias(models.Model): + _inherit = 'mail.alias' + + mailcow_id = fields.One2many('mail.mailcow.alias', 'alias_id') + + def create(self, vals): + alias = super(MailAlias, self).create(vals) + mailcow_alias = self.env['mail.mailcow.alias'].search([('address', '=', alias.alias_name + '@' + alias.alias_domain)]) + if mailcow_alias: + mailcow_alias.write({'active': True}) + else: + self.env['mail.mailcow.alias'].create({ + 'address': alias.alias_name + '@' + alias.alias_domain, + 'goto': alias.alias_defaults.get('email_from', False), + 'alias_id': alias.id, + }) + return alias diff --git a/bemade_mailcow_blacklist/models/mailcow.py b/bemade_mailcow_blacklist/models/mailcow.py new file mode 100644 index 0000000..ca0624d --- /dev/null +++ b/bemade_mailcow_blacklist/models/mailcow.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, exceptions +import requests +import logging + +_logger = logging.getLogger(__name__) + +class MailMailcow(models.AbstractModel): + _name = 'mail.mailcow' + _description = 'Mailcow API' + + def get_credentials(self): + params = self.env['ir.config_parameter'].sudo() + return { + 'base_url': params.get_param('mailcow.base_url'), + 'api_key': params.get_param('mailcow.api_key'), + } + + def api_request(self, endpoint, method='GET', data=None): + creds = self.get_credentials() + url = creds['base_url'] + endpoint + headers = { + 'accept': 'application/json', + 'X-API-Key': creds['api_key'], + 'Content-Type': 'application/json', + } + + try: + if method == 'GET': + response = requests.get(url, headers=headers) + elif method == 'POST': + response = requests.post(url, headers=headers, json=data) + elif method == 'DELETE': + response = requests.delete(url, headers=headers) + elif method == 'PUT': + response = requests.put(url, headers=headers, json=data) + + response.raise_for_status() + + except requests.exceptions.HTTPError as error: + _logger.error('HTTP error occurred: %s', error) + return False + except Exception as error: + _logger.error('An error occurred: %s', error) + return False + + return response.json() diff --git a/bemade_mailcow_blacklist/models/mailcow_alias.py b/bemade_mailcow_blacklist/models/mailcow_alias.py new file mode 100644 index 0000000..8164b0f --- /dev/null +++ b/bemade_mailcow_blacklist/models/mailcow_alias.py @@ -0,0 +1,97 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError +import logging + +_logger = logging.getLogger(__name__) + + +class MailcowAlias(models.Model): + _name = 'mail.mailcow.alias' + _description = 'Mailcow Alias' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _rec_name = 'address' + + address = fields.Char(string='Alias Address', required=True, tracking=True) + goto = fields.Char(string='Alias Destination', required=True, tracking=True) + active = fields.Boolean(string='Active', default=True, tracking=True) + catchall = fields.Boolean(string='Catchall', default=False, tracking=True) + alias_id = fields.Many2one(comodel_name='mail.alias', string='Alias') + + _sql_constraints = [ + ('address_unique', 'UNIQUE(address)', 'The alias address must be unique!'), + ] + + @api.model + def create(self, vals): + alias = super(MailcowAlias, self).create(vals) + + if 'mc_id' not in vals: + data = { + "active": str(int(alias.active)), + "address": alias.address, + "goto": alias.goto, + "private_comment": f"Created by {self.env.user.name} on {fields.Datetime.now()}", + "public_comment": "Alias created in Odoo" + } + result = self.env['mail.mailcow'].api_request('api/v1/add/alias', 'POST', data) + if not result: + raise ValidationError(_("Failed to create alias on Mailcow server.")) + + return alias + + def unlink(self): + for record in self: + endpoint = f"api/v1/delete/alias/{record.mc_id}" + result = self.env['mail.mailcow'].api_request(endpoint, 'POST') + if not result: + raise ValidationError(_("Failed to delete alias on Mailcow server.")) + return super(MailcowAlias, self).unlink() + + def write(self, vals): + for record in self: + data = { + "attr": { + "active": str(int(vals.get('active', record.active))), + "address": vals.get('address', record.address), + "goto": vals.get('goto', record.goto), + "private_comment": f"Modified by {self.env.user.name} on {fields.Datetime.now()}", + "public_comment": f"Alias modified in Odoo. Changes: {vals}", + }, + "items": [record.mc_id] + } + result = self.env['mail.mailcow'].api_request('api/v1/edit/alias', 'POST', data) + if not result: + raise ValidationError(_("Failed to update alias on Mailcow server.")) + + return super(MailcowAlias, self).write(vals) + + @api.model + def sync_aliases(self): + """ + Synchronize the list of aliases from Mailcow server with Odoo. + For each alias fetched from Mailcow server, it tries to find a matching record + in Odoo. If it doesn't exist, it creates a new record. + """ + endpoint = 'api/v1/get/alias/all' + mailcow_aliases = self.api_request(endpoint) + + if not mailcow_aliases: + return + + for mc_alias in mailcow_aliases: + domain = mc_alias['domain'] + alias_domain = self.env['ir.config_parameter'].sudo().get_param('mail.catchall.domain') + if domain == alias_domain: + alias = self.search([('address', '=', mc_alias['address'])], limit=1) + if not alias: + self.create({ + 'address': mc_alias['address'], + 'active': mc_alias['active'] == '1', + 'goto': mc_alias['goto'], + 'domain': mc_alias['domain'] + }) + else: + alias.write({ + 'active': mc_alias['active'] == '1', + 'goto': mc_alias['goto'] + }) diff --git a/bemade_mailcow_blacklist/models/mailcow_blacklist.py b/bemade_mailcow_blacklist/models/mailcow_blacklist.py new file mode 100644 index 0000000..e3f9eeb --- /dev/null +++ b/bemade_mailcow_blacklist/models/mailcow_blacklist.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api +import logging + +_logger = logging.getLogger(__name__) + +class MailcowBlacklist(models.Model): + _name = 'mail.mailcow.blacklist' + _description = 'Mailcow Blacklist' + _inherit = ['mail.mailcow', 'mail.thread', 'mail.activity.mixin'] + + email = fields.Char(string='Email', required=True, track_visibility='onchange') + prefid = fields.Integer(string='Mailcow ID', required=True, track_visibility='onchange') + + @api.model + def create(self, vals): + """ + Overridden create method to add the new blacklist entry to the Mailcow server. + """ + res = super().create(vals) + domain = self.env['res.config.settings'].get_values()['mail.catchall.domain'] + + endpoint_add = 'api/v1/add/domain-policy' + endpoint_get_bl = f"api/v1/get/policy_bl_domain/{domain}" + data = { + 'domain': domain, + 'object_from': res.email, + 'object_list': 'bl' + } + res.api_request(endpoint_add, 'POST', data) + _logger.info(f'Added {vals["email"]} to Mailcow blacklist') + + res.api_request(endpoint_get_bl, 'GET', None) + + return res + + def write(self, vals): + """ + Overridden write method to update the blacklist entry on the Mailcow server. + """ + old_email = self.email + res = super().write(vals) + if 'email' in vals: + delete_endpoint = 'api/v1/delete/domain-policy' + add_endpoint = 'api/v1/add/domain-policy' + delete_data = { + 'items': [old_email] + } + add_data = { + 'items': [vals['email']] + } + self.api_request(delete_endpoint, 'POST', delete_data) + self.api_request(add_endpoint, 'POST', add_data) + _logger.info(f'Updated {old_email} to {vals["email"]} in Mailcow blacklist') + return res + + def unlink(self): + """ + Overridden unlink method to remove the blacklist entry from the Mailcow server. + """ + for record in self: + endpoint = 'api/v1/delete/blacklist' + data = { + 'items': [record.email] + } + record.api_request(endpoint, 'POST', data) + _logger.info(f'Removed {record.email} from Mailcow blacklist') + return super().unlink() diff --git a/bemade_mailcow_blacklist/models/mailcow_mailbox.py b/bemade_mailcow_blacklist/models/mailcow_mailbox.py new file mode 100644 index 0000000..ad45c44 --- /dev/null +++ b/bemade_mailcow_blacklist/models/mailcow_mailbox.py @@ -0,0 +1,134 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError +import logging +import secrets +import string + +_logger = logging.getLogger(__name__) + + +class MailcowMailbox(models.Model): + _name = 'mail.mailcow.mailbox' + _inherit = ['mail.mailcow', 'mail.thread', 'mail.activity.mixin'] + _description = 'Mailcow Mailbox' + + name = fields.Char(track_visibility='onchange') + address = fields.Char(compute='_compute_address', store=True, readonly=True, track_visibility='onchange') + local_part = fields.Char(required=True, track_visibility='onchange') + domain = fields.Char(required=True, track_visibility='onchange') + active = fields.Boolean(default=True, track_visibility='onchange') + user_id = fields.Many2one('res.users', ondelete='cascade', track_visibility='onchange') + password = fields.Char(readonly=True, track_visibility='onchange') + + @api.depends('local_part', 'domain') + def _compute_address(self): + for record in self: + record.address = f"{record.local_part}@{record.domain}" + + def sync_mailboxes(self): + """ + Synchronize Mailcow mailboxes with Odoo + """ + endpoint = 'api/v1/get/mailbox/all' + data = self.api_request(endpoint) + if data: + for item in data: + domain = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + if item['domain'] == domain: + self.create({ + 'name': item['name'], + 'local_part': item['local_part'], + 'domain': item['domain'], + 'active': item['active'], + 'password': item['password'], + }) + + @api.model + def create(self, vals): + """ + Override the create function to create a Mailcow mailbox whenever a user is created in Odoo. + """ + password = secrets.token_hex(16) + vals['password'] = password + + # Check if email exists on Mailcow + endpoint = f"api/v1/get/mailbox/{vals['address']}" + response = self.api_request(endpoint) + + if not response: + # If email does not exist on Mailcow, create it + endpoint = 'api/v1/add/mailbox' + data = { + 'local_part': vals['local_part'], + 'domain': vals['domain'], + 'name': vals['name'], + 'password': password, + 'password2': password, + 'quota': "3072", + 'active': "1", + 'force_pw_update': "0", + 'tls_enforce_in': "0", + 'tls_enforce_out': "0", + } + self.api_request(endpoint, method='POST', data=data) + _logger.info(f'Mailbox {vals["address"]} has been created on Mailcow server') + + return super().create(vals) + + def write(self, vals): + """ + Override the write function to update a Mailcow mailbox whenever a user is updated in Odoo. + """ + if 'active' in vals or 'local_part' in vals or 'domain' in vals: + endpoint = f'api/v1/edit/mailbox/{self.address}' + data = { + 'items': [self.address], + 'attr': { + 'active': '1' if vals.get('active', self.active) else '0', + 'local_part': vals.get('local_part', self.local_part), + 'domain': vals.get('domain', self.domain), + } + } + self.api_request(endpoint, method='POST', data=data) + _logger.info(f'Mailbox {self.address} has been updated on Mailcow server') + + return super().write(vals) + + def unlink(self): + """ + Override the unlink function to delete a Mailcow mailbox whenever a user is deleted in Odoo. + """ + endpoint = f'api/v1/delete/mailbox/{self.address}' + self.api_request(endpoint, method='POST') + _logger.info(f'Mailbox {self.address} has been deleted on Mailcow server') + + return super().unlink() + + + def create_mailbox_for_user(self, user): + """ + Function to create a Mailcow mailbox for a new Odoo user. + + Parameters: + user (res.users): The newly created Odoo user + + Returns: + mail.mailcow.mailbox: The newly created Mailcow mailbox + """ + data = { + 'local_part': user.login.split('@')[0], + 'domain': user.login.split('@')[1], + 'name': user.name, + 'password': user.password, + } + endpoint = 'api/v1/add/mailbox' + self.api_request(endpoint, method='POST', data=data) + _logger.info(f'Mailbox for user {user.login} has been created on Mailcow server') + + pw_bundle = seld.env['password.bundle'].search([('name', '=', user.name)]) + self.env['password.key'].create_mailbox_for_user(res) + + return self.create({ + 'address': user.login, + 'user_id': user.id, + }) \ No newline at end of file diff --git a/bemade_mailcow_blacklist/models/res_config_settings.py b/bemade_mailcow_blacklist/models/res_config_settings.py new file mode 100644 index 0000000..1b91c36 --- /dev/null +++ b/bemade_mailcow_blacklist/models/res_config_settings.py @@ -0,0 +1,15 @@ +from odoo import fields, models + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + mailcow_base_url = fields.Char( + string="Mailcow Base URL", + help="URL for the Mailcow server API", + config_parameter='mailcow.base_url', + ) + mailcow_api_key = fields.Char( + string="Mailcow API Key", + help="API key for the Mailcow server", + config_parameter='mailcow.api_key', + ) diff --git a/bemade_mailcow_blacklist/models/res_users.py b/bemade_mailcow_blacklist/models/res_users.py new file mode 100644 index 0000000..a3130ea --- /dev/null +++ b/bemade_mailcow_blacklist/models/res_users.py @@ -0,0 +1,14 @@ +from odoo import models, fields, api + +class ResUsers(models.Model): + _inherit = 'res.users' + + mailcow_mailbox = fields.Boolean(string='Mailcow Mailbox', default=False) + + def create(self, vals): + res = super(ResUsers, self).create(vals) + + if vals.get('mailcow_mailbox'): + self.env['mail.mailcow.mailbox'].create_mailbox_for_user(res) + + return res diff --git a/bemade_mailcow_blacklist/security/ir.model.access.csv b/bemade_mailcow_blacklist/security/ir.model.access.csv new file mode 100644 index 0000000..4ab848e --- /dev/null +++ b/bemade_mailcow_blacklist/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_mail_mailcow_alias_all,access_mail_mailcow_alias,model_mail_mailcow_alias,base.group_user,1,0,0,0 +access_mail_mailcow_alias_admin,access_mail_mailcow_alias,model_mail_mailcow_alias,base.group_system,1,1,1,1 +access_mail_mailcow_mailbox_all,access_mail_mailcow_mailbox,model_mail_mailcow_mailbox,base.group_user,1,0,0,0 +access_mail_mailcow_mailbox_admin,access_mail_mailcow_mailbox,model_mail_mailcow_mailbox,base.group_system,1,1,1,1 +access_mail_mailcow_blacklist_all,access_mail_mailcow_blacklist,model_mail_mailcow_blacklist,base.group_user,1,0,0,0 +access_mail_mailcow_blacklist_admin,access_mail_mailcow_blacklist,model_mail_mailcow_blacklist,base.group_system,1,1,1,1 diff --git a/bemade_mailcow_blacklist/views/mailcow_alias_views.xml b/bemade_mailcow_blacklist/views/mailcow_alias_views.xml new file mode 100644 index 0000000..a4c48fb --- /dev/null +++ b/bemade_mailcow_blacklist/views/mailcow_alias_views.xml @@ -0,0 +1,58 @@ + + + + mailcow.alias.view.tree + mail.mailcow.alias + + + + + + + + + + + + mailcow.alias.view.form + mail.mailcow.alias + +
+ + + + + + + + + + + + + +
+
+
+ + + Aliases + ir.actions.act_window + mail.mailcow.alias + tree,form + +

+ Create a new Alias +

+

+ Add the details of the new Alias. +

+
+
+ + +
diff --git a/bemade_mailcow_blacklist/views/mailcow_blacklist_views.xml b/bemade_mailcow_blacklist/views/mailcow_blacklist_views.xml new file mode 100644 index 0000000..0a4de07 --- /dev/null +++ b/bemade_mailcow_blacklist/views/mailcow_blacklist_views.xml @@ -0,0 +1,52 @@ + + + + mailcow.blacklist.view.tree + mail.mailcow.blacklist + + + + + + + + + mailcow.blacklist.view.form + mail.mailcow.blacklist + +
+ + + + + + + + + + +
+
+
+ + + Blacklist + ir.actions.act_window + mail.mailcow.blacklist + tree,form + +

+ Create a new Blacklist Entry +

+

+ Add the details of the new Blacklist Entry. +

+
+
+ + +
diff --git a/bemade_mailcow_blacklist/views/mailcow_mailbox_views.xml b/bemade_mailcow_blacklist/views/mailcow_mailbox_views.xml new file mode 100644 index 0000000..ef19f68 --- /dev/null +++ b/bemade_mailcow_blacklist/views/mailcow_mailbox_views.xml @@ -0,0 +1,67 @@ + + + + mailcow.mailbox.view.tree + mail.mailcow.mailbox + + + + + + + + + + + + + + mailcow.mailbox.view.form + mail.mailcow.mailbox + +
+ + + + + + + + + + + + + + + +
+
+
+ + + Mailboxes + ir.actions.act_window + mail.mailcow.mailbox + tree,form + +

+ Create a new Mailbox +

+

+ Add the details of the new Mailbox. +

+
+
+ + + + +
diff --git a/bemade_mailcow_blacklist/views/res_config_settings_views.xml b/bemade_mailcow_blacklist/views/res_config_settings_views.xml new file mode 100644 index 0000000..98cc026 --- /dev/null +++ b/bemade_mailcow_blacklist/views/res_config_settings_views.xml @@ -0,0 +1,39 @@ + + + + res.config.settings.form.inherit.mailcow + res.config.settings + + + +
+

Mailcow Settings

+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
diff --git a/bemade_mailcow_blacklist/views/res_users_views.xml b/bemade_mailcow_blacklist/views/res_users_views.xml new file mode 100644 index 0000000..04217b8 --- /dev/null +++ b/bemade_mailcow_blacklist/views/res_users_views.xml @@ -0,0 +1,20 @@ + + + + res.users.mailcow.form + res.users + + + + + + + + + + + + + + + diff --git a/bemade_mailcow_blacklist/views/templates.xml b/bemade_mailcow_blacklist/views/templates.xml new file mode 100644 index 0000000..cea6b39 --- /dev/null +++ b/bemade_mailcow_blacklist/views/templates.xml @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/bemade_mailcow_blacklist/views/views.xml b/bemade_mailcow_blacklist/views/views.xml new file mode 100644 index 0000000..45f432e --- /dev/null +++ b/bemade_mailcow_blacklist/views/views.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file