diff --git a/bemade_helpdesk_mailcow_blacklist/__init__.py b/bemade_helpdesk_mailcow_blacklist/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/bemade_helpdesk_mailcow_blacklist/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/bemade_helpdesk_mailcow_blacklist/__manifest__.py b/bemade_helpdesk_mailcow_blacklist/__manifest__.py new file mode 100644 index 0000000..9aad068 --- /dev/null +++ b/bemade_helpdesk_mailcow_blacklist/__manifest__.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Helpdesk Mailcow Blacklist', + 'version': '1.0.0', + 'category': 'Administration', + 'summary': 'Module for adding blacklist functionality to the helpdesk.', + 'description': """ + Helpdesk Mailcow Blacklist + + This module extends the functionality of the Helpdesk and Mailcow Blacklist modules by adding an action to blacklist the sender of a Helpdesk ticket. + + Main Features: + - Adds a button on Helpdesk tickets to blacklist the email sender. + - This button is only visible when an email is associated with the ticket. + - Blacklisting an email sender automatically moves the Helpdesk ticket to a new 'Spam' stage, which is marked as closed. + - The 'Spam' stage is automatically created by this module. + """, + 'sequence': 10, + 'license': 'GPL-3', + 'author': 'Bemade', + 'website': 'https://www.bemade.org', + 'depends': [ + 'helpdesk', + 'bemade_mailcow_integration' + ], + 'data': [ + # 'security/ir.model.access.csv', + 'data/helpdesk_stages.xml', + 'views/helpdesk_ticket_views.xml', + ], + 'demo': [ + 'demo/demo.xml' + ], + 'installable': True, + 'application': False, + 'auto_install': False +} diff --git a/bemade_helpdesk_mailcow_blacklist/data/helpdesk_stages.xml b/bemade_helpdesk_mailcow_blacklist/data/helpdesk_stages.xml new file mode 100644 index 0000000..3ff0278 --- /dev/null +++ b/bemade_helpdesk_mailcow_blacklist/data/helpdesk_stages.xml @@ -0,0 +1,10 @@ + + + + + Spam + 50 + True + + + diff --git a/bemade_helpdesk_mailcow_blacklist/demo/demo.xml b/bemade_helpdesk_mailcow_blacklist/demo/demo.xml new file mode 100644 index 0000000..b8f2a97 --- /dev/null +++ b/bemade_helpdesk_mailcow_blacklist/demo/demo.xml @@ -0,0 +1,27 @@ + + + + + + Demo User + demo_user + demo_user + demo_user@example.com + + + + + Demo Helpdesk Team + + + + + Demo Helpdesk Ticket + + + demo_sender@example.com + + This is a demo ticket for testing the blacklist feature. + + + diff --git a/bemade_helpdesk_mailcow_blacklist/models/__init__.py b/bemade_helpdesk_mailcow_blacklist/models/__init__.py new file mode 100644 index 0000000..deff245 --- /dev/null +++ b/bemade_helpdesk_mailcow_blacklist/models/__init__.py @@ -0,0 +1 @@ +from . import helpdesk_ticket \ No newline at end of file diff --git a/bemade_helpdesk_mailcow_blacklist/models/helpdesk_ticket.py b/bemade_helpdesk_mailcow_blacklist/models/helpdesk_ticket.py new file mode 100644 index 0000000..c98ae98 --- /dev/null +++ b/bemade_helpdesk_mailcow_blacklist/models/helpdesk_ticket.py @@ -0,0 +1,27 @@ +from odoo import api, fields, models +from odoo.exceptions import UserError +import re + +class HelpdeskTicket(models.Model): + _inherit = 'helpdesk.ticket' + + def action_add_blacklist(self): + self.ensure_one() + email_regex = r'<([^<>]+)>' + + email_to_blacklist = re.findall(email_regex, self.email)[0] + + # Create a new blacklist record + blacklist = self.env['mail.mailcow.blacklist'].create({ + 'email': email_to_blacklist, + }) + + # Assign 'spam' as a closed stage + spam_stage = self.env.ref('bemade_helpdesk_mailcow_blacklist.helpdesk_stage_spam') + + if spam_stage: + self.stage_id = spam_stage.id + else: + raise UserError(_('The Spam stage does not exist.')) + + return True diff --git a/bemade_helpdesk_mailcow_blacklist/models/res_partner.py b/bemade_helpdesk_mailcow_blacklist/models/res_partner.py new file mode 100644 index 0000000..b11a071 --- /dev/null +++ b/bemade_helpdesk_mailcow_blacklist/models/res_partner.py @@ -0,0 +1,11 @@ +from odoo import models, api + +class ResPartner(models.Model): + _inherit = 'res.partner' + + @api.multi + def send_validation_email(self): + for partner in self: + if not partner.email_validated: + # Assuming you have a method called send_validation_email that sends the validation email. + partner.send_validation_email() diff --git a/bemade_helpdesk_mailcow_blacklist/security/ir.model.access.csv b/bemade_helpdesk_mailcow_blacklist/security/ir.model.access.csv new file mode 100644 index 0000000..4d035d4 --- /dev/null +++ b/bemade_helpdesk_mailcow_blacklist/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_bemade_helpdesk_mailcow_blacklist,access_bemade_helpdesk_mailcow_blacklist,model_bemade_helpdesk_mailcow_blacklist,base.group_user,1,1,1,1 diff --git a/bemade_helpdesk_mailcow_blacklist/views/helpdesk_ticket_views.xml b/bemade_helpdesk_mailcow_blacklist/views/helpdesk_ticket_views.xml new file mode 100644 index 0000000..cc02fe6 --- /dev/null +++ b/bemade_helpdesk_mailcow_blacklist/views/helpdesk_ticket_views.xml @@ -0,0 +1,13 @@ + + + + helpdesk.ticket.form.mailcow + helpdesk.ticket + + + + + + + + diff --git a/bemade_mailcow_integration/__init__.py b/bemade_mailcow_integration/__init__.py new file mode 100644 index 0000000..cde864b --- /dev/null +++ b/bemade_mailcow_integration/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models diff --git a/bemade_mailcow_integration/__manifest__.py b/bemade_mailcow_integration/__manifest__.py new file mode 100644 index 0000000..14c39bb --- /dev/null +++ b/bemade_mailcow_integration/__manifest__.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Mailcow Integration', + 'version': '1.0.0', + 'category': 'Administration', + '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': 'GPL-3', + 'author': 'Bemade', + 'website': 'https://www.bemade.org', + 'depends': [ + 'hr', + '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', + ], + "assets": { + "web.assets_backend": [ + "bemade_mailcow_integration/static/src/js/mailcow.js", + ], + "web.assets_qweb": [ + "bemade_mailcow_integration/static/src/xml/mailcow_templates.xml", + ], + }, + 'installable': True, + 'application': False, + 'auto_install': False +} diff --git a/bemade_mailcow_integration/controllers/controllers.py b/bemade_mailcow_integration/controllers/controllers.py new file mode 100644 index 0000000..bd69c50 --- /dev/null +++ b/bemade_mailcow_integration/controllers/controllers.py @@ -0,0 +1,13 @@ +from odoo import http +from odoo.http import request + +class EmailValidationController(http.Controller): + + @http.route('/email_validation//', type='http', auth='public', website=True) + def email_validation(self, partner_id, token): + partner = request.env['res.partner'].sudo().browse(partner_id) + if partner and partner.validation_token == token: + partner.sudo().write({'email_validated': True}) + return "Email validated!" + else: + return "Invalid validation link." diff --git a/bemade_mailcow_integration/demo/demo.xml b/bemade_mailcow_integration/demo/demo.xml new file mode 100644 index 0000000..60b6c36 --- /dev/null +++ b/bemade_mailcow_integration/demo/demo.xml @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/bemade_mailcow_integration/models/__init__.py b/bemade_mailcow_integration/models/__init__.py new file mode 100644 index 0000000..0d900ad --- /dev/null +++ b/bemade_mailcow_integration/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_integration/models/mail_alias.py b/bemade_mailcow_integration/models/mail_alias.py new file mode 100644 index 0000000..66a1a98 --- /dev/null +++ b/bemade_mailcow_integration/models/mail_alias.py @@ -0,0 +1,29 @@ +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') + + @api.model + def create(self, vals): + alias = super(MailAlias, self).create(vals) + + alias_domain = self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain"), + catchall_alias = self.env["ir.config_parameter"].sudo().get_param("mail.catchall.alias"), + + alias_domain = alias_domain[0] + catchall_alias = catchall_alias[0] + + if alias_domain: + mailcow_alias = self.env['mail.mailcow.alias'].search([('address', '=', alias.alias_name + '@' + alias_domain)]) + if mailcow_alias: + mailcow_alias.write({'active': True}) + else: + self.env['mail.mailcow.alias'].create({ + 'address': alias.alias_name + '@' + alias_domain, + 'goto': catchall_alias + '@' + alias_domain, + 'alias_id': alias.id, + }) + return alias diff --git a/bemade_mailcow_integration/models/mailcow.py b/bemade_mailcow_integration/models/mailcow.py new file mode 100644 index 0000000..2b9b9d8 --- /dev/null +++ b/bemade_mailcow_integration/models/mailcow.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ +import requests +import logging +from odoo.exceptions import ValidationError + + +_logger = logging.getLogger(__name__) + +class MailMailcow(models.AbstractModel): + _name = 'mail.mailcow' + _description = 'Mailcow API' + + @property + def get_credentials(self): + params = self.env['ir.config_parameter'].sudo() + + base_url = params.get_param('mailcow.base_url') + api_key = params.get_param('mailcow.api_key') + + if not base_url or not api_key: + _logger.error('No API key or base URL is set in the system parameters') + raise ValidationError(_("No API key or base URL is set in the system parameters. Please set one and try again.")) + # return False + else: + return { + 'base_url': base_url, + 'api_key': api_key + } + + def api_request(self, endpoint, method='GET', data=None): + creds = self.get_credentials + if not creds: + return False + 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_integration/models/mailcow_alias.py b/bemade_mailcow_integration/models/mailcow_alias.py new file mode 100644 index 0000000..560b11b --- /dev/null +++ b/bemade_mailcow_integration/models/mailcow_alias.py @@ -0,0 +1,108 @@ +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.mailcow', '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') + create_date_mailcow = fields.Datetime(string='Created on Mailcow', readonly=True) + modify_date_mailcow = fields.Datetime(string='Modified on Mailcow', readonly=True) + mc_id = fields.Integer(string='Mailcow ID', readonly=True) + + _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": bool(alias.active), + "address": alias.address, + "catchall": bool(alias.catchall), + "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: + #pass + 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([('mc_id', '=', mc_alias['id'])], limit=1) + if not alias: + self.create({ + 'address': mc_alias['address'], + 'active': bool(mc_alias['active']), + 'goto': mc_alias['goto'], + 'mc_id': mc_alias['id'], + 'create_date_mailcow': mc_alias['created'], + 'modify_date_mailcow': mc_alias['modified'], + }) + else: + alias.write({ + 'address': mc_alias['address'], + 'active': bool(mc_alias['active']), + 'goto': mc_alias['goto'], + 'create_date_mailcow': mc_alias['created'], + 'modify_date_mailcow': mc_alias['modified'], + }) diff --git a/bemade_mailcow_integration/models/mailcow_blacklist.py b/bemade_mailcow_integration/models/mailcow_blacklist.py new file mode 100644 index 0000000..3e3ba81 --- /dev/null +++ b/bemade_mailcow_integration/models/mailcow_blacklist.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api +import logging +import json + +_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, tracking=True) + mc_id = fields.Integer(string='Mailcow ID', required=True, tracking=True) + + @api.model + def create(self, vals): + """ + Overridden create method to add the new blacklist entry to the Mailcow server. + """ + domain = self.env['ir.config_parameter'].sudo().get_param('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': vals['email'], + 'object_list': 'bl' + } + + self.api_request(endpoint_add, 'POST', data) + _logger.info(f'Added {vals["email"]} to Mailcow blacklist') + + bl_emails = self.api_request(endpoint_get_bl, 'GET', None) + + mc_id = [d['prefid'] for d in bl_emails if d['value'] == vals['email']] + vals['mc_id'] = mc_id[0] + res = super().create(vals) + 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. + """ + endpoint = '/api/v1/delete/domain-policy' + for record in self: + data = json.dumps(record.mc_id) + self.api_request(endpoint, 'POST', data) + _logger.info(f'Removed {record.email} from Mailcow blacklist') + return super().unlink() + + @api.model + def sync_blacklist(self): + """ + Function to sync Mailcow blacklist with Odoo's blacklist. It fetches the list from Mailcow + and updates Odoo's blacklist accordingly. + + Returns: + bool: True if the sync is successful, False otherwise + """ + params = self.env['ir.config_parameter'].sudo() + domain = params.get_param('mail.catchall.domain') + endpoint = f'/api/v1/get/policy_bl_domain/{domain}' + try: + response = self.api_request(endpoint, 'GET') + if response: + for item in response: + existing = self.search([('mc_id', '=', item['prefid'])], limit=1) + if existing: + if existing.email != item['value']: + existing.write({'email': item['valuw']}) + else: + self.create({ + 'email': item['value'], + 'mc_id': item['prefid'], + }) + return True + except Exception as e: + _logger.error('An error occurred while syncing blacklist: %s', str(e)) + return False diff --git a/bemade_mailcow_integration/models/mailcow_mailbox.py b/bemade_mailcow_integration/models/mailcow_mailbox.py new file mode 100644 index 0000000..920db47 --- /dev/null +++ b/bemade_mailcow_integration/models/mailcow_mailbox.py @@ -0,0 +1,143 @@ +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' + + def _default_domain(self): + return self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain") + + name = fields.Char(tracking=True) + address = fields.Char(compute='_compute_address', store=True, readonly=True, tracking=True) + local_part = fields.Char(required=True, tracking=True) + domain = fields.Char(required=True, tracking=True, default=_default_domain) + active = fields.Boolean(default=True, tracking=True) + user_id = fields.Many2one('res.users', ondelete='cascade', tracking=True) + password = fields.Char(readonly=True, tracking=True) + + @api.depends('local_part', 'domain') + def _compute_address(self): + for record in self: + record.address = f"{record.local_part}@{record.domain}" + + @api.model + def sync_mailboxes(self): + """ + Synchronize Mailcow mailboxes with Odoo + """ + endpoint = '/api/v1/get/mailbox/all' + data = self.api_request(endpoint) + domain = self.env['ir.config_parameter'].sudo().get_param('mail.catchall.domain') + if data: + for item in data: + if item['domain'] == domain: + mailbox = self.env['mail.mailcow.mailbox'].search([('address', '=', f"{item['local_part']}@{item['domain']}")]) + if not mailbox: + self.create({ + 'name': item['name'], + 'local_part': item['local_part'], + 'domain': item['domain'], + 'active': item['active'], + }) + + @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['local_part']}@{vals['domain']}" + 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['local_part']}@{vals['domain']} 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. + """ + + for mailbox in self: + data = { + 'username': mailbox.address + } + + endpoint = f'/api/v1/delete/mailbox' + mailbox.api_request(endpoint, method='POST', data=data) + _logger.info(f'Mailbox {mailbox.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_integration/models/res_config_settings.py b/bemade_mailcow_integration/models/res_config_settings.py new file mode 100644 index 0000000..07d285a --- /dev/null +++ b/bemade_mailcow_integration/models/res_config_settings.py @@ -0,0 +1,20 @@ +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', + ) + + mailcow_sync_alias = fields.Boolean( + string='Sync Aliases with Odoo', + help='Auto create Aliases in Mailcow from Odoo', + config_parameter='mailcow.sync_alias') \ No newline at end of file diff --git a/bemade_mailcow_integration/models/res_users.py b/bemade_mailcow_integration/models/res_users.py new file mode 100644 index 0000000..884fdbc --- /dev/null +++ b/bemade_mailcow_integration/models/res_users.py @@ -0,0 +1,15 @@ +from odoo import models, fields, api + +class ResUsers(models.Model): + _inherit = 'res.users' + + mailcow_mailbox = fields.Boolean(string='Mailcow Mailbox', default=False) + + @api.model + def create(self, vals): + res = super(ResUsers, self).create(vals) + + if vals.get('mailcow_mailbox', false): + self.env['mail.mailcow.mailbox'].create_mailbox_for_user(res) + + return res diff --git a/bemade_mailcow_integration/security/ir.model.access.csv b/bemade_mailcow_integration/security/ir.model.access.csv new file mode 100644 index 0000000..4ab848e --- /dev/null +++ b/bemade_mailcow_integration/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_integration/static/description/icon.png b/bemade_mailcow_integration/static/description/icon.png new file mode 100644 index 0000000..4c79dc8 Binary files /dev/null and b/bemade_mailcow_integration/static/description/icon.png differ diff --git a/bemade_mailcow_integration/static/description/logomailcow.svg b/bemade_mailcow_integration/static/description/logomailcow.svg new file mode 100644 index 0000000..9ba163f --- /dev/null +++ b/bemade_mailcow_integration/static/description/logomailcow.svg @@ -0,0 +1,187 @@ + + + +image/svg+xml diff --git a/bemade_mailcow_integration/static/src/js/mailcow.js b/bemade_mailcow_integration/static/src/js/mailcow.js new file mode 100644 index 0000000..d1a4ac4 --- /dev/null +++ b/bemade_mailcow_integration/static/src/js/mailcow.js @@ -0,0 +1,89 @@ +/** @odoo-module **/ + +import ListController from 'web.ListController'; +import ListView from 'web.ListView'; +const viewRegistry = require('web.view_registry'); + +const AliasesListController = ListController.extend({ + // buttons_template must match the t-name on the template for the button (static xml) + buttons_template: 'mail.mailcow_aliases_list_view_buttons', + events: _.extend({}, ListController.prototype.events, { + 'click .o_button_sync_aliases': '_onSyncAliases', + }), + // This may need to be async function() if there needs to be an await this._rpc({ ... }); call to not reload early + _onSyncAliases: function () { + this._rpc({ + model: 'mail.mailcow.alias', + method: 'sync_aliases', + args: [], + }).then(() => { + this.reload(); + }); + // Couldn't test this for real, but it runs the action. May need a this.reload() + }, +}); + +const AliasesListView = ListView.extend({ + config: _.extend({}, ListView.prototype.config, { + Controller: AliasesListController, + }), +}); + +// key must match with the js_class attribute of the tree view you want to modify +viewRegistry.add('mail_mailcow_aliases_tree', AliasesListView); + +const BlacklistListController = ListController.extend({ + // buttons_template must match the t-name on the template for the button (static xml) + buttons_template: 'mail.mailcow_blacklist_list_view_buttons', + events: _.extend({}, ListController.prototype.events, { + 'click .o_button_sync_blacklist': '_onSyncBlacklist', + }), + // This may need to be async function() if there needs to be an await this._rpc({ ... }); call to not reload early + _onSyncBlacklist: function () { + this._rpc({ + model: 'mail.mailcow.blacklist', + method: 'sync_blacklist', + args: [], + }).then(() => { + this.reload(); + }); + // Couldn't test this for real, but it runs the action. May need a this.reload() + }, +}); + +const BlacklistListView = ListView.extend({ + config: _.extend({}, ListView.prototype.config, { + Controller: BlacklistListController, + }), +}); + +// key must match with the js_class attribute of the tree view you want to modify +viewRegistry.add('mail_mailcow_blacklist_tree', BlacklistListView); + +const MailboxesListController = ListController.extend({ + // buttons_template must match the t-name on the template for the button (static xml) + buttons_template: 'mail.mailcow_mailbox_list_view_buttons', + events: _.extend({}, ListController.prototype.events, { + 'click .o_button_sync_mailboxes': '_onSyncMailboxes', + }), + // This may need to be async function() if there needs to be an await this._rpc({ ... }); call to not reload early + _onSyncMailboxes: function () { + this._rpc({ + model: 'mail.mailcow.mailbox', + method: 'sync_mailboxes', + args: [], + }).then(() => { + this.reload(); + }); + // Couldn't test this for real, but it runs the action. May need a this.reload() + }, +}); + +const MailboxesListView = ListView.extend({ + config: _.extend({}, ListView.prototype.config, { + Controller: MailboxesListController, + }), +}); + +// key must match with the js_class attribute of the tree view you want to modify +viewRegistry.add('mail_mailcow_mailbox_tree', MailboxesListView); diff --git a/bemade_mailcow_integration/static/src/xml/mailcow_templates.xml b/bemade_mailcow_integration/static/src/xml/mailcow_templates.xml new file mode 100644 index 0000000..80e19b0 --- /dev/null +++ b/bemade_mailcow_integration/static/src/xml/mailcow_templates.xml @@ -0,0 +1,28 @@ + + + + + + Sync Aliases + + + + + + Sync Blacklist + + + + + + Sync Mailboxes + + + + \ No newline at end of file diff --git a/bemade_mailcow_integration/tests/__init__.py b/bemade_mailcow_integration/tests/__init__.py new file mode 100644 index 0000000..cc6beea --- /dev/null +++ b/bemade_mailcow_integration/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mailcow diff --git a/bemade_mailcow_integration/tests/test_mailcow.py b/bemade_mailcow_integration/tests/test_mailcow.py new file mode 100644 index 0000000..53ed6ca --- /dev/null +++ b/bemade_mailcow_integration/tests/test_mailcow.py @@ -0,0 +1,12 @@ +from odoo.tests.common import TransactionCase, tagged + + +@tagged('-at_install', 'post_install') +class TestMailcow(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def test_new_mail_alias(self): + model_id = self.env['ir.model']._get('res.partner').id + self.alias = self.env['mail.alias'].create({'alias_name': 'test', 'alias_model_id': model_id}) diff --git a/bemade_mailcow_integration/views/mailcow_alias_views.xml b/bemade_mailcow_integration/views/mailcow_alias_views.xml new file mode 100644 index 0000000..367c6d2 --- /dev/null +++ b/bemade_mailcow_integration/views/mailcow_alias_views.xml @@ -0,0 +1,76 @@ + + + + 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. + + + + + + + + Sync Aliases + + code + model.sync_aliases() + + + + + tree + + + + + diff --git a/bemade_mailcow_integration/views/mailcow_blacklist_views.xml b/bemade_mailcow_integration/views/mailcow_blacklist_views.xml new file mode 100644 index 0000000..fe51427 --- /dev/null +++ b/bemade_mailcow_integration/views/mailcow_blacklist_views.xml @@ -0,0 +1,66 @@ + + + + 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. + + + + + + + + Sync Blacklist + + code + model.sync_blacklist() + + + + + tree + + + + diff --git a/bemade_mailcow_integration/views/mailcow_mailbox_views.xml b/bemade_mailcow_integration/views/mailcow_mailbox_views.xml new file mode 100644 index 0000000..fc233fd --- /dev/null +++ b/bemade_mailcow_integration/views/mailcow_mailbox_views.xml @@ -0,0 +1,82 @@ + + + + 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. + + + + + + + + + + Sync Mailboxes + + code + model.sync_mailboxes() + + + + + tree + + + + + diff --git a/bemade_mailcow_integration/views/res_config_settings_views.xml b/bemade_mailcow_integration/views/res_config_settings_views.xml new file mode 100644 index 0000000..d641fc7 --- /dev/null +++ b/bemade_mailcow_integration/views/res_config_settings_views.xml @@ -0,0 +1,40 @@ + + + + res.config.settings.form.inherit.mailcow + res.config.settings + + + + + Mailcow Settings + + + + + Base URL for Mailcow server + + + + + + + Mailcow API Key + + + + + + + Auto create Aliases in Mailcow from Odoo + + + + + + + + + + + diff --git a/bemade_mailcow_integration/views/res_users_views.xml b/bemade_mailcow_integration/views/res_users_views.xml new file mode 100644 index 0000000..366beda --- /dev/null +++ b/bemade_mailcow_integration/views/res_users_views.xml @@ -0,0 +1,19 @@ + + + + res.users.mailcow.form + res.users + + + + + + + + + + + + + + diff --git a/bemade_mailcow_integration/views/templates.xml b/bemade_mailcow_integration/views/templates.xml new file mode 100644 index 0000000..cea6b39 --- /dev/null +++ b/bemade_mailcow_integration/views/templates.xml @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/bemade_mailcow_integration/views/views.xml b/bemade_mailcow_integration/views/views.xml new file mode 100644 index 0000000..45f432e --- /dev/null +++ b/bemade_mailcow_integration/views/views.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bemade_time_off_follower/__init__.py b/bemade_time_off_follower/__init__.py new file mode 100644 index 0000000..9a7e03e --- /dev/null +++ b/bemade_time_off_follower/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/bemade_time_off_follower/__manifest__.py b/bemade_time_off_follower/__manifest__.py new file mode 100644 index 0000000..76eea2d --- /dev/null +++ b/bemade_time_off_follower/__manifest__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +{ + "name": "Time Off Alternative Follower", + "version": "15.0.0.0.1", + "category": "Extra Tools", + 'summary': 'Add Alternative Follower When Receiving Message While On Time Off', + "description": """ + Add Alternative Follower When Receiving Message While On Time Off + """, + "author": "Bemade", + 'website': 'https://www.bemade.org', + "depends": [ + 'hr_holidays', + 'mail' + ], + "data": [ + 'views/hr_leave_views.xml', + ], + "auto_install": False, + "installable": True, + 'license': 'OPL-1' +} diff --git a/bemade_time_off_follower/models/__init__.py b/bemade_time_off_follower/models/__init__.py new file mode 100644 index 0000000..fa00ea0 --- /dev/null +++ b/bemade_time_off_follower/models/__init__.py @@ -0,0 +1,2 @@ +from . import hr_leave +from . import mail_thread diff --git a/bemade_time_off_follower/models/hr_leave.py b/bemade_time_off_follower/models/hr_leave.py new file mode 100644 index 0000000..09750a0 --- /dev/null +++ b/bemade_time_off_follower/models/hr_leave.py @@ -0,0 +1,10 @@ +from odoo import models, fields, api + +class HrLeave(models.Model): + _inherit = 'hr.leave' + + alternate_follower_id = fields.Many2one( + comodel_name='res.users', + string='Alternate Follower', + help='User who will receive a copy of all communications related to this leave', + ) diff --git a/bemade_time_off_follower/models/mail_thread.py b/bemade_time_off_follower/models/mail_thread.py new file mode 100644 index 0000000..8186da4 --- /dev/null +++ b/bemade_time_off_follower/models/mail_thread.py @@ -0,0 +1,39 @@ +from odoo import models, api, fields +import logging + +_logger = logging.getLogger(__name__) + + +class MailThread(models.AbstractModel): + _inherit = 'mail.thread' + + def _notify_compute_recipients(self, message, msg_vals): + recipients = super(MailThread, self)._notify_compute_recipients(message, msg_vals) + + for recipient in recipients: + user = self.env['res.users'].search([('partner_id', '=', recipient['id'])], limit=1) + if user: + employee = self.env['hr.employee'].search([('user_id', '=', user.id)], limit=1) + + if employee: + employee_id = employee.id + leaves = self.sudo().env['hr.leave'].search([ + ('state', '=', 'validate'), + ('date_from', '<=', fields.Date.today()), + ('employee_id', '=', employee_id), + ('date_to', '>=', fields.Date.today()), + ]) + for leave in leaves: + if leave.employee_id.user_id.partner_id.id == recipient['id'] and leave.alternate_follower_id: + _logger.info( + f"adding {leave.alternate_follower_id.partner_id.name} as follower for {employee.name} while on time off.") + recipients.append({ + 'id': leave.alternate_follower_id.partner_id.id, + 'active': True, + 'share': False, + 'groups': leave.alternate_follower_id.groups_id.ids, + 'notif': 'inbox', + 'type': 'user' + }) + + return recipients diff --git a/bemade_time_off_follower/views/hr_leave_views.xml b/bemade_time_off_follower/views/hr_leave_views.xml new file mode 100644 index 0000000..a992f48 --- /dev/null +++ b/bemade_time_off_follower/views/hr_leave_views.xml @@ -0,0 +1,13 @@ + + + + hr.leave.form.view.inherit + hr.leave + + + + + + + + diff --git a/bemade_user_password_bundle/__init__.py b/bemade_user_password_bundle/__init__.py new file mode 100644 index 0000000..5305644 --- /dev/null +++ b/bemade_user_password_bundle/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models \ No newline at end of file diff --git a/bemade_user_password_bundle/__manifest__.py b/bemade_user_password_bundle/__manifest__.py new file mode 100644 index 0000000..893ccf6 --- /dev/null +++ b/bemade_user_password_bundle/__manifest__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +{ + 'name': "User Password Bundle", + + 'summary': """ + Module to create password bundles and provide access to them for admins of a newly created user. + """, + + 'description': """ + This module automate the creation of a password bundle for new users in Odoo 15 and changes the default admin + ownership of bundle to admin/setting group instead of the bundle creator. + """, + + 'author': "Bemade", + 'website': "https://www.bemade.org", + 'category': 'Technical', + 'version': '0.1', + 'license': 'AGPL-3', + 'depends': ['odoo_password_manager'], + + 'data': [ + ], + 'demo': [ + ], +} diff --git a/bemade_user_password_bundle/models/__init__.py b/bemade_user_password_bundle/models/__init__.py new file mode 100644 index 0000000..3c6d7ca --- /dev/null +++ b/bemade_user_password_bundle/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import hr_employee +from . import password_bundle diff --git a/bemade_user_password_bundle/models/hr_employee.py b/bemade_user_password_bundle/models/hr_employee.py new file mode 100644 index 0000000..6517fb9 --- /dev/null +++ b/bemade_user_password_bundle/models/hr_employee.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api + + +class HrEmployee(models.Model): + _inherit = 'hr.employee' + + @api.model + def create(self, vals): + newemployee = super(HrEmployee, self).create(vals) + pw_bundle = self.env['password.bundle'].create({ + 'name': newemployee.name, + 'notes': f"Created new employee Password Bundle for {newemployee.name}" + }) + self.env['password.access'].create({ + 'bundle_id': pw_bundle.id, + 'user_id': newemployee.user_id.id, + 'access_level': 'full', + }) + return newemployee \ No newline at end of file diff --git a/bemade_user_password_bundle/models/password_bundle.py b/bemade_user_password_bundle/models/password_bundle.py new file mode 100644 index 0000000..7a93cd2 --- /dev/null +++ b/bemade_user_password_bundle/models/password_bundle.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api + + +class password_bundle(models.Model): + _inherit = 'password.bundle' + _name = 'password.bundle' + + @api.model + def _default_access_admin_ids(self): + """ + Default method for access_ids + """ + values = { + 'access_level': 'admin', + 'group_id': self.env.ref('base.group_system').id, + } + return [(0, 0, values)] + + # BV: UGLY HACK + # I should be able to remove this field, but I can't just override the default function + # _default_access_ids, so I rename it _default_access_admin_ids and override the th field + + access_ids = fields.One2many( + "password.access", + "bundle_id", + default=_default_access_admin_ids, + ) +
+ Create a new Alias +
+ Add the details of the new Alias. +
+ Create a new Blacklist Entry +
+ Add the details of the new Blacklist Entry. +
+ Create a new Mailbox +
+ Add the details of the new Mailbox. +