| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
|
|
| import binascii, base64, re, json, os, sys, time, shutil, socket, io, math |
| from genericpath import isfile |
| from datetime import datetime, timedelta, timezone |
| import requests |
| import psutil |
| import pytz |
| from flask import request |
| from mailModel.base import Base |
|
|
| try: |
| from BTPanel import cache |
| except: |
| import cachelib |
|
|
| cache = cachelib.SimpleCache() |
| import traceback |
|
|
| if sys.version_info[0] == 3: |
| from importlib import reload |
|
|
| if sys.version_info[0] == 2: |
| reload(sys) |
| sys.setdefaultencoding('utf-8') |
|
|
| sys.path.append('/www/server/panel') |
|
|
| sys.path.append("class/") |
|
|
| from mod.base import public_aap as public |
|
|
| import mailModel.server_init as msi |
|
|
| try: |
| import dns.resolver |
| except: |
| if os.path.exists('/www/server/panel/pyenv'): |
| public.ExecShell('/www/server/panel/pyenv/bin/pip install dnspython') |
| else: |
| public.ExecShell('pip install dnspython') |
| import dns.resolver |
|
|
| import smtplib |
| from email.mime.multipart import MIMEMultipart |
| from email.mime.base import MIMEBase |
| from email.mime.text import MIMEText |
| from email.encoders import encode_base64 |
| from email.utils import COMMASPACE, formatdate, formataddr, make_msgid |
| from email.header import Header |
|
|
|
|
| try: |
| import jwt |
| except: |
| public.ExecShell('btpip install pyjwt') |
| import jwt |
|
|
|
|
| |
|
|
|
|
|
|
| class SendMail: |
| ''' |
| 发件类 |
| ''' |
| __setupPath = '/www/server/panel/plugin/mail_sys' |
| _session_conf = __setupPath + '/session.json' |
|
|
| def __init__(self, username, password, server, port=25, usettls=False): |
| self._session = self._get_session() |
| self.mailUser = username |
| self.mailPassword = password |
| self.smtpServer = server |
| self.smtpPort = port |
| self.mailServer = smtplib.SMTP(self.smtpServer, self.smtpPort) |
| if usettls: |
| self.mailServer.starttls() |
| self.mailServer.ehlo() |
| self.mailServer.login(self.mailUser, self.mailPassword) |
| self.msg = MIMEMultipart() |
| self.mailbox_list = [ |
| 'gmail.com', |
| 'googlemail.com', |
| 'hotmail.com', |
| 'outlook.com', |
| 'yahoo.com', |
| 'protonmail.com', |
| 'zoho.com', |
| 'icloud.com', |
| ] |
|
|
| def __del__(self): |
| self.mailServer.close() |
|
|
| |
| def update_init(self, name): |
| self.msg = MIMEMultipart() |
| sender = formataddr((name, self.mailUser)) |
| self.msg['From'] = sender |
| self.msg['Date'] = formatdate(localtime=True) |
|
|
| def nwe_msg(self, msgid): |
| msg = MIMEMultipart() |
| msg['Subject'] = self.msg['Subject'] |
| msg['From'] = self.msg['From'] |
| msg['Date'] = self.msg['Date'] |
| msg['Message-ID'] = msgid |
|
|
| |
| |
| |
| |
|
|
| msg.set_payload(self.msg.get_payload()) |
|
|
| return msg |
|
|
| def _get_session(self): |
| session = public.readFile(self._session_conf) |
| if session: |
| session = json.loads(session) |
| else: |
| session = {} |
| return session |
|
|
| def setMailInfo(self, name, subject, text, attachmentFilePaths): |
|
|
| self.msg['Subject'] = subject |
| sender = formataddr((name, self.mailUser)) |
| self.msg['From'] = sender |
| self.msg['Date'] = formatdate(localtime=True) |
|
|
| self.msg.attach(MIMEText(text, 'html', _charset="utf-8")) |
| for attachmentFilePath in attachmentFilePaths: |
| self.msg.attach(self.addAttachmentFromFile(attachmentFilePath)) |
|
|
| def setMailInfo_one(self, name): |
| sender = formataddr((name, self.mailUser)) |
| self.msg['From'] = sender |
| self.msg['Date'] = formatdate(localtime=True) |
|
|
| |
| def setMailInfo_two(self, subject, text, attachmentFilePaths): |
| |
| self.msg['Subject'] = subject |
| self.msg.attach(MIMEText(text, 'html', _charset="utf-8")) |
| for attachmentFilePath in attachmentFilePaths: |
| self.msg.attach(self.addAttachmentFromFile(attachmentFilePath)) |
|
|
| |
| def addAttachment(self, filename, filedata): |
| part = MIMEBase('application', "octet-stream") |
| part.set_payload(filedata) |
| encode_base64(part) |
| part.add_header('Content-Disposition', 'attachment; filename="%s"' % str(Header(filename, 'utf8'))) |
| self.msg.attach(part) |
|
|
| |
| def addAttachmentFromFile(self, attachmentFilePath): |
| part = MIMEBase('application', "octet-stream") |
| part.set_payload(open(attachmentFilePath, "rb").read()) |
| encode_base64(part) |
| part.add_header('Content-Disposition', 'attachment; filename="%s"' % str(Header(attachmentFilePath, 'utf8'))) |
| return part |
|
|
| |
| def count_receiveUsers(self, receiveUsers): |
|
|
| data = { |
| 'gmail': 0, |
| 'outlook': 0, |
| 'yahoo.com': 0, |
| 'icloud.com': 0, |
| 'other': 0, |
| } |
|
|
| for i in receiveUsers: |
| _, domain = i.lower().split('@') |
| domain_key = domain if domain in self.mailbox_list else 'other' |
|
|
| if domain_key in ['gmail.com', 'googlemail.com']: |
| domain_key = 'gmail' |
| if domain_key in ['hotmail.com', 'outlook.com']: |
| domain_key = 'outlook' |
| |
| if domain_key != 'other': |
| data[domain_key] += 1 |
|
|
| |
| count_sent = '/www/server/panel/plugin/mail_sys/count_sent_domain.json' |
| if not os.path.exists(count_sent): |
| data_d = { |
| 'gmail': 0, |
| 'outlook': 0, |
| 'yahoo.com': 0, |
| 'icloud.com': 0, |
| 'other': 0, |
| } |
| else: |
| try: |
| data_d = public.readFile(count_sent) |
| data_d = json.loads(data_d) |
| |
| if 'outher' in data_d: |
| data_d['other'] = data_d.pop('outher') |
| except: |
|
|
| data_d = { |
| 'gmail': 0, |
| 'outlook': 0, |
| 'yahoo.com': 0, |
| 'icloud.com': 0, |
| 'other': 0, |
| } |
|
|
| result = {} |
| |
| for key in data.keys(): |
| result[key] = data[key] + data_d[key] |
| |
| public.writeFile(count_sent, json.dumps(result)) |
|
|
| is_ok = True |
| |
| for key, value in result.items(): |
| if key == 'other': |
| continue |
|
|
| if value > 5000: |
| is_ok = False |
|
|
| return is_ok |
|
|
| |
| def count_receiveUsers_one(self, receiveUsers): |
|
|
| data = { |
| 'gmail': 0, |
| 'outlook': 0, |
| 'yahoo.com': 0, |
| 'icloud.com': 0, |
| 'other': 0, |
| } |
| receiveUser = receiveUsers[0] |
| _, domain = receiveUser.lower().split('@') |
| domain_key = domain if domain in self.mailbox_list else 'other' |
|
|
| if domain_key in ['gmail.com', 'googlemail.com']: |
| domain_key = 'gmail' |
| if domain_key in ['hotmail.com', 'outlook.com']: |
| domain_key = 'outlook' |
|
|
| |
| if domain_key != 'other': |
| data[domain_key] += 1 |
|
|
| |
| count_sent = '/www/server/panel/plugin/mail_sys/count_sent_domain.json' |
| if not os.path.exists(count_sent): |
| data_d = { |
| 'gmail': 0, |
| 'outlook': 0, |
| 'yahoo.com': 0, |
| 'icloud.com': 0, |
| 'other': 0, |
| } |
| else: |
| try: |
| data_d = public.readFile(count_sent) |
| data_d = json.loads(data_d) |
| |
| if 'outher' in data_d: |
| data_d['other'] = data_d.pop('outher') |
| except: |
|
|
| data_d = { |
| 'gmail': 0, |
| 'outlook': 0, |
| 'yahoo.com': 0, |
| 'icloud.com': 0, |
| 'other': 0, |
| } |
|
|
| result = {} |
| |
| for key in data.keys(): |
| result[key] = data[key] + data_d[key] |
| |
| |
|
|
| is_ok = True |
| |
| for key, value in result.items(): |
| if key == 'other': |
| continue |
| |
| if value > 5000: |
| is_ok = False |
| return is_ok |
| |
| public.writeFile(count_sent, json.dumps(result)) |
| return is_ok |
|
|
| def sendMail(self, receiveUsers, domain, is_record, msgid=None): |
|
|
| |
| try: |
| key = '{0}:{1}'.format(domain, 'PTR') |
| isptr = self._session[key]['status'] |
| if not isptr: |
| |
| if not self.count_receiveUsers_one(receiveUsers): |
| return public.returnMsg(False, '超过发送限制,请联系IP提供商进行PTR记录') |
| except: |
| pass |
|
|
| if msgid: |
| msgid = msgid |
| else: |
| msgid = make_msgid() |
| |
| msg = self.nwe_msg(msgid) |
| msg['To'] = COMMASPACE.join(receiveUsers) |
|
|
| try: |
| try: |
| result = self.mailServer.sendmail(self.mailUser, receiveUsers, msg.as_string()) |
|
|
| except Exception as e: |
| public.print_log(public.get_error_info()) |
| return public.returnMsg(False, '发送邮件失败,错误原因[{0}]'.format(str(e))) |
|
|
| |
| if is_record: |
| |
| local_part, domain = self.mailUser.split('@') |
| dir_path = '/www/vmail/{0}/{1}/.Sent/cur'.format(domain, local_part) |
| if not os.path.isdir(dir_path): |
| os.makedirs(dir_path) |
| file_name = public.GetRandomString(36) |
| if file_name in [item.split(':')[0] for item in os.listdir(dir_path)]: |
| file_name = public.GetRandomString(54) |
| public.writeFile(os.path.join(dir_path, file_name), msg.as_string()) |
| self.set_owner_and_group(os.path.join(dir_path, file_name), 'vmail', 'mail') |
|
|
| |
| del self.msg['To'] |
| del msg['To'] |
| del msg['Message-ID'] |
| return public.returnMsg(True, "发送邮件成功") |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
| return public.returnMsg(False, '发送邮件失败,错误原因[{0}]'.format(str(e))) |
|
|
| def parse_queue_id(self, receiveUsers): |
| |
| try: |
| |
| output, err = public.ExecShell('mailq') |
|
|
| pattern = re.compile( |
| r'(?P<queue_id>\S+)\*?\s+\d+\s+\w{3}\s\w{3}\s+\d+\s+\d+:\d+:\d+\s+\S+\s*[\s\S]*?\n\s+' + re.escape( |
| receiveUsers[0])) |
| |
| match = pattern.search(output) |
|
|
| if match: |
| |
| queue_id = match.group('queue_id').rstrip('*') |
|
|
| return queue_id |
| else: |
|
|
| return None |
|
|
| except Exception as e: |
| public.print_log(public.get_error_info()) |
| return None |
|
|
| def set_owner_and_group(self, path, user, group): |
| ''' |
| 检测目录所有者和组 并更改 |
| :param path: 目录或文件 user: 用户, group: 组 |
| :return: |
| ''' |
| import os |
| import pwd |
| import grp |
| try: |
| |
| stat_info = os.stat(path) |
| current_uid = stat_info.st_uid |
| current_gid = stat_info.st_gid |
|
|
| |
| vmail_uid = pwd.getpwnam(user).pw_uid |
| mail_gid = grp.getgrnam(group).gr_gid |
| if current_uid == vmail_uid and current_gid == mail_gid: |
| return |
| |
| os.chown(path, vmail_uid, mail_gid) |
| |
| |
| |
| |
| |
| except: |
| pass |
|
|
| def _get_pubilc_ip(self): |
|
|
| try: |
| |
| url = 'https://ifconfig.me/ip' |
| opener = requests.get(url) |
| m_str = opener.text |
| ip_address = re.search(r'\d+.\d+.\d+.\d+', m_str).group(0) |
| c_ip = public.check_ip(ip_address) |
| if not c_ip: |
| a, e = public.ExecShell("curl ifconfig.me") |
| return a |
| return ip_address |
| except: |
| filename = '/www/server/panel/data/iplist.txt' |
| ip_address = public.readFile(filename).strip() |
| if public.check_ip(ip_address): |
| return ip_address |
| else: |
| return None |
|
|
| def _get_all_ip(self): |
| |
| public_ip = self._get_pubilc_ip() |
| net_info = psutil.net_if_addrs() |
| addr = [] |
| for i in net_info.values(): |
| addr.append(i[0].address) |
| locataddr = public.readFile('/www/server/panel/data/iplist.txt') |
| if not locataddr: |
| locataddr = "" |
| ip_address = locataddr.strip() |
| if ip_address not in addr: |
| addr.append(ip_address) |
| if public_ip not in addr: |
| addr.append(public_ip) |
| return addr |
|
|
| def check_ptr_domain(self, domain): |
| ''' |
| 检测IP地址是否有PTR记录 |
| :param ip_address: IP地址字符串 |
| :return: bool |
| ''' |
|
|
| try: |
| ip_addresses = self._get_all_ip() |
| ip_addresses = [ip for ip in ip_addresses if ip != '127.0.0.1'] |
|
|
| found_ptr_record = False |
| result = None |
| for ip_address in ip_addresses: |
| if ':' in ip_address: |
| reverse_domain = self._ipv6_to_ptr(ip_address) |
| else: |
| reverse_domain = '.'.join(reversed(ip_address.split('.'))) + '.in-addr.arpa' |
|
|
| resolver = dns.resolver.Resolver() |
| resolver.timeout = 5 |
| resolver.lifetime = 10 |
|
|
| try: |
| |
| |
| result = resolver.query(reverse_domain, 'PTR') |
| found_ptr_record = True |
|
|
| break |
| |
| except: |
| continue |
| |
| if found_ptr_record: |
| values = [str(rdata.target).rstrip('.') for rdata in result] |
|
|
| for i in values: |
| if i.endswith(domain): |
| return True |
| else: |
| continue |
| return False |
| return False |
|
|
| except Exception as e: |
| public.print_log(public.get_error_info()) |
| return False |
|
|
| def _ipv6_to_ptr(self, ipv6_address): |
|
|
| parts = ipv6_address.split(':') |
| normalized_parts = [part.zfill(4) for part in parts] |
| |
| normalized_address = ''.join(normalized_parts) |
| |
| reversed_address = normalized_address[::-1] |
| |
| ptr_address_parts = list(reversed_address) |
| ptr_address = '.'.join(ptr_address_parts) |
| ptr_address += '.ip6.arpa' |
| |
|
|
| return ptr_address |
|
|
|
|
| class main(Base): |
| __setupPath = '/www/server/panel/plugin/mail_sys' |
| _session_conf = __setupPath + '/session.json' |
| _forward_conf = __setupPath + '/forward.json' |
| _save_conf = __setupPath + '/save_day.json' |
| postfix_main_cf = "/etc/postfix/main.cf" |
| |
| postfix_recipient_blacklist = '/etc/postfix/blacklist' |
| _check_time = 86400 |
| _check_time2 = 60 |
| |
| unsubscribe_path = __setupPath + "/setinfo.json" |
|
|
| def __init__(self): |
| |
| self.db_files = { |
| 'postfixadmin': '/www/vmail/postfixadmin.db', |
| 'postfixmaillog': '/www/vmail/postfixmaillog.db', |
| 'mail_unsubscribe': '/www/vmail/mail_unsubscribe.db', |
| 'abnormal_recipient': '/www/vmail/abnormal_recipient.db' |
| } |
| |
| self.sys_v = self.get_linux_distribution().lower() |
| self._session = self._get_session() |
| self.in_bulk_path = '/www/server/panel/data/mail/in_bulk' |
| self.blacklist_tips = '/www/server/panel/plugin/mail_sys/data/blacklist_tips' |
| self.blacklist_alarm_switch = '/www/server/panel/plugin/mail_sys/data/blacklist_alarm_switch' |
| if not os.path.exists(self.in_bulk_path): |
| os.makedirs(self.in_bulk_path) |
|
|
| if not os.path.exists("{}/content".format(self.in_bulk_path)): |
| os.mkdir("{}/content".format(self.in_bulk_path)) |
|
|
| |
| |
| |
|
|
| |
| self.check_domain_column() |
|
|
| |
| self.is_pflogsumm = self.is_pflogsumm_installed() |
| |
| self._roundcube_ssl_status() |
| |
| self.remove_old_cron() |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
|
|
| |
| |
|
|
| self.maillog_path = '/var/log/maillog' |
| if "ubuntu" in public.get_linux_distribution().lower(): |
| self.maillog_path = '/var/log/mail.log' |
|
|
| |
| self.update_task_count_table() |
| |
| |
| self._sync_blacklist_to_unsubscribe_db() |
|
|
| |
| def update_task_count_table(self): |
| path = '/www/server/panel/data/update_mail_task_count_table.pl' |
| if os.path.exists(path): |
| return |
|
|
| if not os.path.exists('/www/vmail/postfixadmin.db'): |
| public.writeFile(path, '') |
| return |
|
|
| |
| with self.M("task_count") as obj: |
| total = obj.count() |
|
|
| if not total: |
| public.writeFile(path, '') |
| return |
|
|
| try: |
|
|
| |
| create_table_sql = ''' |
| CREATE TABLE IF NOT EXISTS `task_count_new` ( |
| `id` INTEGER PRIMARY KEY AUTOINCREMENT, |
| `task_id` INTEGER NOT NULL, |
| `recipient` varchar(320) NOT NULL, |
| `delay` varchar(320) NOT NULL, |
| `delays` varchar(320) NOT NULL, |
| `dsn` varchar(320) NOT NULL, |
| `relay` text NOT NULL, |
| `domain` varchar(320) NOT NULL, |
| `status` varchar(255) NOT NULL, |
| `err_info` text NOT NULL, |
| `created` INTEGER NOT NULL DEFAULT 0, |
| UNIQUE (`task_id`, `recipient`) -- 联合唯一约束 |
| ); |
| ''' |
|
|
| rename_table_sql1 = ''' |
| ALTER TABLE `task_count` RENAME TO `task_count_bak`; |
| ''' |
| |
| rename_table_sql2 = ''' |
| ALTER TABLE `task_count_new` RENAME TO `task_count`; |
| ''' |
|
|
| |
| with self.M("") as obj: |
| obj.execute(create_table_sql) |
|
|
| |
| with self.M("task_count") as obj: |
| alldata = obj.field('task_id,recipient,delay,delays,dsn,relay,domain,status,err_info').select() |
|
|
| |
| with public.S("task_count_new", "/www/vmail/postfixadmin.db") as obj: |
| aa = obj.insert_all(alldata, option='IGNORE') |
| |
| |
| with self.M("") as obj: |
| |
| obj.execute(rename_table_sql1) |
| |
| obj.execute(rename_table_sql2) |
|
|
| |
| public.writeFile(path, '') |
| except: |
| public.print_log(public.get_error_info()) |
|
|
| def check_black(self): |
| try: |
| with open(self.postfix_recipient_blacklist, 'r') as file: |
| emails = file.read().splitlines() |
| except Exception as e: |
| emails = [] |
|
|
| if not emails: |
| |
| st = self.recipient_blacklist_open(False) |
| if st: |
| public.ExecShell('systemctl reload postfix') |
|
|
| def login_roundcube(self, args): |
| ''' |
| 一键登录 roundcube webmail |
| :param args: rc_user账号 rc_pass密码 |
| :return: url |
| ''' |
| if not hasattr(args, 'rc_user') or args.get('rc_user/s', "") == "": |
| return self.self.return_msg(public.returnMsg(False, '参数 rc_user 错误')) |
| if not hasattr(args, 'rc_pass') or args.get('rc_pass/s', "") == "": |
| return self.return_msg(public.returnMsg(False, '参数 rc_pass 错误')) |
|
|
| rc_user = args.rc_user |
| rc_pass = args.rc_pass |
|
|
| |
| with self.M("mailbox") as obj: |
| un = obj.where('username=?', rc_user).count() |
| if un <= 0: |
| return self.return_msg(public.returnMsg(False, '用户不存在')) |
|
|
| |
| |
| |
| info = self.get_roundcube_status(None)['data'] |
| if not info['status']: |
| return self.self.return_msg(public.returnMsg(False, '请先安装roundcube')) |
|
|
| site_name = info['site_name'] |
| token = public.GetRandomString(16) |
| |
| login_name = public.GetRandomString(5) + '.php' |
| roundcube_path = '/www/wwwroot/' + site_name + '/' |
| file = roundcube_path + login_name |
| |
| tmp_file = "/www/server/panel/class/mailModel/roundcube_autologin.php" |
| if not os.path.exists(tmp_file): |
| return self.return_msg(public.returnMsg(False, '缺少必要文件')) |
| data_info = public.readFile(tmp_file) |
| |
| data_info = data_info.replace('__WEBMAIL_ROUNDCUBE_RANDOM_TOKEN__', token) |
| data_info = data_info.replace('__WEBMAIL_ROUNDCUBE_USERNAME__', rc_user) |
| data_info = data_info.replace('__WEBMAIL_ROUNDCUBE_PASSWORD__', rc_pass) |
| data_info = data_info.replace('__WEBMAIL_ROUNDCUBE_LOGINPHP_PATH__', file) |
| |
| public.writeFile(file, data_info) |
| url = "{}/{}?_aap_token={}".format(site_name, login_name, token) |
| return self.return_msg(url) |
|
|
| |
| |
|
|
| |
| def get_domain_name(self, args): |
| with self.M("domain") as obj: |
| data_list = obj.order('created desc').field("domain").select() |
|
|
| |
| data_list = [i['domain'] for i in data_list] |
| return self.return_msg(data_list) |
|
|
| def get_mailbox(self, args): |
| if "domain" in args and args.domain: |
| data = self.M('mailbox').where('domain=?', args.domain).field('username').select() |
| else: |
| data = self.M('mailbox').field('username').select() |
| return self.return_msg(data) |
|
|
| def get_domainip(self, args): |
| ''' |
| 查询域名和ip 用于安装 webmail |
| :param args: |
| :return: |
| ''' |
| with self.M("domain") as obj: |
| data_list = obj.field('domain,a_record').select() |
| |
| public.print_log(data_list) |
| all_list = [] |
| |
| for i in data_list: |
| ip = self._session['{}:A'.format(i['a_record'])]['value'] |
| all_list.append(ip) |
| all_list.append(i['domain']) |
| domainip = list(set(all_list)) |
| return self.return_msg(domainip) |
|
|
| |
| def _pflogsumm_data_treating(self, output, timezone=None): |
|
|
| ''' |
| 分析命令执行后的数据 |
| :param args: output 命令返回内容 |
| :param args: timezone 时区 默认为系统时区 为'utc'时 使用0时区 提交数据需要 |
| :return: data list |
| ''' |
|
|
| import public.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
| try: |
| return SendMailBulk()._pflogsumm_data_treating(output, timezone) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return [] |
|
|
| |
| def is_pflogsumm_installed(self): |
| if os.path.exists('/usr/sbin/pflogsumm'): |
| return True |
| else: |
| return False |
|
|
| |
| |
| |
| |
| def get_today_count(self, args): |
| if not self.is_pflogsumm: |
| errinfo = "" |
| if not os.path.exists('/usr/sbin/pflogsumm'): |
| if self.sys_v == 'centos7': |
| errinfo = 'yum install postfix-pflogsumm -y' |
| elif self.sys_v == 'centos8': |
| errinfo = 'yum install postfix-pflogsumm -y' |
| elif self.sys_v == 'ubuntu': |
| errinfo = 'apt install pflogsumm -y' |
|
|
| return self.return_msg(public.returnMsg(False, '请先运行[{}]安装pflogsumm'.format(errinfo))) |
|
|
| else: |
| self.is_pflogsumm = True |
|
|
| |
| cache_key = 'mail_sys:get_today_count' |
| cache = public.cache_get(cache_key) |
| if cache: |
| return self.return_msg(cache) |
|
|
| output, err = public.ExecShell( |
| 'pflogsumm -d today --verbose-msg-detail --zero-fill --iso-date-time --rej-add-from {}'.format(self.maillog_path)) |
| data = self._pflogsumm_data_treating(output) |
|
|
| public.cache_set(cache_key, data, 30) |
| |
| public.run_thread(self.get_yesterday_count) |
|
|
| |
| self.task_cut_maillog() |
| return self.return_msg(data) |
|
|
| def get_monthly_quota_statistics(self, args): |
| |
| data = { |
| "sent": 0, |
| "free_quota": 0, |
| "pack_use": 0, |
| "pack_total": 0, |
| "packages": [], |
| "available": 0, |
| } |
| try: |
| import public.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
|
|
| m_sent = SendMailBulk()._get_month_senduse() |
| pack = SendMailBulk()._get_user_pack_quota() |
| free_quota = SendMailBulk()._get_user_free_quota() |
|
|
| data = { |
| "sent": m_sent, |
| "free_quota": free_quota, |
| "pack_use": pack['used'], |
| "pack_total": pack['total'], |
| "packages": pack['packages'], |
| "available": pack['available'], |
| } |
| except: |
| public.print_log(public.get_error_info()) |
|
|
| return self.return_msg(data) |
|
|
| |
| def get_yesterday_count(self): |
|
|
| import public.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
| try: |
| return SendMailBulk().get_yesterday_count() |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return {} |
|
|
| |
| def get_month_senduse(self): |
| |
| dnum = self.get_data_month_count(None) |
| pnum = self.get_pflogsumm_month_count(None) |
| |
| cnum = 0 |
| senduse = dnum if dnum >= pnum else pnum |
| |
| if senduse < cnum: |
|
|
| return cnum |
| else: |
| return senduse |
|
|
| |
| def get_data_month_count(self, args): |
| ''' |
| 数据库 获取本月发件数 |
| :param args: int |
| :return: |
| ''' |
|
|
| import public.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
| try: |
|
|
| return SendMailBulk().get_data_month_count(args) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return {} |
|
|
| |
| def get_pflogsumm_month_count(self, args): |
| ''' |
| 命令 获取本月发件数 |
| :param args: int |
| :return: |
| ''' |
|
|
| import public.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
| try: |
|
|
| return SendMailBulk().get_pflogsumm_month_count(args) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return {} |
|
|
| |
| def get_data_month_list(self, args): |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
|
|
| strat = int(args.strat) |
| end = int(args.end) |
|
|
| try: |
| |
| total_fields = "sum(received) as received, sum(delivered) as delivered, sum(deferred) as deferred, sum(bounced) as bounced, sum(rejected) as rejected, sum(delivered+bounced+rejected) as sentall" |
| with self.M("log_analysis") as obj: |
| query = obj.field(total_fields).where('time between ? and ?', (strat, end)).find() |
| query2 = obj.where('time between ? and ?', (strat, end)).order('time desc').select() |
|
|
| |
|
|
| |
| data = { |
| "hourly_stats": query2, |
| "stats_dict": query, |
| } |
| return data |
| except: |
| public.print_log(public.get_error_info()) |
|
|
| def get_postconf(self): |
| if os.path.exists("/usr/sbin/postconf"): |
| return "/usr/sbin/postconf" |
| elif os.path.exists("/sbin/postconf"): |
| return "/sbin/postconf" |
| else: |
| return "postconf" |
|
|
| def get_linux_distribution(self): |
| distribution = 'ubuntu' |
| redhat_file = '/etc/redhat-release' |
| if os.path.exists(redhat_file): |
| try: |
| tmp = public.readFile(redhat_file).split()[3][0] |
| distribution = 'centos{}'.format(tmp) |
| except: |
| distribution = 'centos7' |
| elif not os.path.exists('/usr/bin/apt-get'): |
| distribution = 'centos7' |
| return distribution |
|
|
| def check_mail_sys(self, args): |
| if os.path.exists('/etc/postfix/sqlite_virtual_domains_maps.cf'): |
| public.ExecShell('{} -e "message_size_limit = 102400000"'.format(self.get_postconf())) |
| |
| result = public.readFile(self.postfix_main_cf) |
| if not result: |
| return self.return_msg(public.returnMsg(False, "找不到postfix配置文件")) |
| result = re.search(r"\n*mydestination\s*=(.+)", result) |
| if not result: |
| return self.return_msg(public.returnMsg(False, |
| "postfix配置文件中找不到mydestination配置项")) |
| result = result.group(1) |
| if 'localhost' in result or '$myhostname' in result or '$mydomain' in result: |
| public.ExecShell('{} -e "mydestination =" && systemctl restart postfix'.format(self.get_postconf())) |
| |
| dovecot_conf = public.readFile("/etc/dovecot/dovecot.conf") |
| if not dovecot_conf or not re.search(r"\n*protocol\s*imap", |
| dovecot_conf): |
| return self.return_msg(public.returnMsg(False, '配置dovecot失败')) |
| |
| |
| |
| |
|
|
| return self.return_msg(public.returnMsg(True, '邮局系统已经存在,重装之前请先卸载!')) |
| else: |
| return self.return_msg(public.returnMsg(False, '之前没有安装过邮局系统,请放心安装!')) |
|
|
| def check_mail_env(self, args): |
| return self.return_msg(msi.mail_server_init().check_env()) |
|
|
| def change_to_rspamd(self, args): |
| msi.change_to_rspamd().main() |
| return self.return_msg(public.returnMsg(True, "设置成功")) |
|
|
| def install_rspamd(self, args): |
| a, e = public.ExecShell("bash {}/install.sh rspamd".format( |
| self.__setupPath)) |
| return self.return_msg(public.returnMsg(True, "安装成功")) |
|
|
| |
| def setup_mail_sys(self, args): |
| ''' |
| 安装邮局系统主函数 |
| :param args: |
| :return: |
| ''' |
| res = msi.mail_server_init().setup_mail_sys(args) |
| |
| self.check_black() |
| |
| self.task_cut_maillog() |
| return self.return_msg(res) |
|
|
| |
| def _check_smtp_port(self): |
| import telnetlib |
|
|
| host_list = ['mx1.qq.com', 'mx2.qq.com', 'mx3.qq.com', 'smtp.gmail.com'] |
| for host in host_list: |
| try: |
| tn = telnetlib.Telnet(host, 25, timeout=5) |
| if tn: return True |
| except: |
| continue |
| return False |
|
|
| |
| def _get_pubilc_ip(self): |
|
|
| try: |
| |
| url = 'https://ifconfig.me/ip' |
| opener = requests.get(url) |
| m_str = opener.text |
| ip_address = re.search(r'\d+.\d+.\d+.\d+', m_str).group(0) |
| c_ip = public.check_ip(ip_address) |
| if not c_ip: |
| a, e = public.ExecShell("curl ifconfig.me") |
| return a |
| return ip_address |
| except: |
| filename = '/www/server/panel/data/iplist.txt' |
| ip_address = public.readFile(filename).strip() |
| if public.check_ip(ip_address): |
| return ip_address |
| else: |
| return None |
|
|
| def _check_a(self, hostname): |
| ''' |
| 检测主机名是否有A记录 |
| :param hostname: |
| :return: |
| ''' |
| ipaddress = self._get_all_ip() |
| if not ipaddress: return False |
| key = '{0}:{1}'.format(hostname, 'A') |
| now = int(time.time()) |
| value = "" |
| error_ip = "" |
| try: |
| if key in self._session and self._session[key]["status"] != 0: |
| v_time = now - int(self._session[key]["v_time"]) |
| if v_time < self._check_time: |
| value = self._session[key]["value"] |
| if not value: |
| |
| resolver = dns.resolver.Resolver() |
| resolver.timeout = 5 |
| resolver.lifetime = 10 |
| try: |
| result = resolver.query(hostname, 'A') |
| except: |
| result = resolver.resolve(hostname, 'A') |
| for i in result.response.answer: |
| for j in i.items: |
| error_ip = j |
| if str(j).strip() in ipaddress: |
| value = str(j).strip() |
| if value: |
| self._session[key] = { |
| "status": 1, |
| "v_time": now, |
| "value": value |
| } |
| return True |
| if str(type(error_ip)).find("dns.rdtypes.IN.A") != -1: |
| self._session[key] = { |
| "status": 0, |
| "v_time": now, |
| "value": error_ip.to_text() |
| } |
| else: |
| self._session[key] = { |
| "status": 0, |
| "v_time": now, |
| "value": error_ip |
| } |
| return False |
| except: |
| public.print_log(public.get_error_info()) |
| self._session[key] = {"status": 0, "v_time": now, "value": value} |
| return False |
|
|
| def repair_postfix(self, args=None): |
| if self.sys_v == 'centos7': |
| msi.mail_server_init().install_postfix_on_centos7() |
| elif self.sys_v == 'centos8': |
| msi.mail_server_init().install_postfix_on_centos8() |
| elif self.sys_v == 'ubuntu': |
| msi.mail_server_init().install_postfix_on_ubuntu() |
| return self.return_msg(msi.mail_server_init().conf_postfix()) |
|
|
| def repair_dovecot(self, args=None): |
| status = False |
| if os.path.exists('/etc/dovecot/conf.d/10-ssl.conf'): |
| if os.path.exists('/tmp/10-ssl.conf_aap_bak'): |
| os.remove('/tmp/10-ssl.conf_aap_bak') |
| shutil.move('/etc/dovecot/conf.d/10-ssl.conf', '/tmp/10-ssl.conf_aap_bak') |
| if self.sys_v == 'centos7': |
| if msi.mail_server_init().install_dovecot_on_centos7(): |
| status = True |
| elif self.sys_v == 'centos8': |
| msi.mail_server_init().install_postfix_on_centos8() |
| status = True |
| elif self.sys_v == 'ubuntu': |
| msi.mail_server_init().install_dovecot_on_ubuntu() |
| status = True |
| if os.path.exists('/tmp/10-ssl.conf_aap_bak') and os.path.exists('/etc/dovecot/conf.d'): |
| if os.path.exists("/etc/dovecot/conf.d/10-ssl.conf"): |
| os.remove('/etc/dovecot/conf.d/10-ssl.conf') |
| shutil.move('/tmp/10-ssl.conf_aap_bak', |
| '/etc/dovecot/conf.d/10-ssl.conf') |
| return self.return_msg(public.returnMsg(status, |
| "修复{}!".format("成功" if status else "失败"))) |
|
|
| |
| def repair_service_conf(self, args=None): |
| service_name = args.service |
| if service_name.lower() not in ['postfix', 'dovecot', 'rspamd']: |
| return self.return_msg(public.returnMsg(False, '服务名不正确')) |
| if service_name == 'postfix': |
| self.repair_postfix() |
| elif service_name == 'dovecot': |
| self.repair_dovecot() |
| elif service_name == 'rspamd': |
| msi.mail_server_init().setup_rspamd() |
| return self.return_msg(public.returnMsg(True, '修复成功')) |
|
|
| |
| def get_service_status(self, args=None): |
| data = {} |
| data['change_rspamd'] = True if "smtpd_milters = inet:127.0.0.1:11332" not in public.readFile( |
| "/etc/postfix/main.cf") else False |
| data['postfix'] = public.process_exists('master', '/usr/libexec/postfix/master') |
| data['dovecot'] = public.process_exists('dovecot', '/usr/sbin/dovecot') |
| data['rspamd'] = public.process_exists('rspamd', '/usr/bin/rspamd') |
| data['opendkim'] = public.process_exists('opendkim', '/usr/sbin/opendkim') |
| if "ubuntu" in self.sys_v: |
| data['postfix'] = public.process_exists('master', '/usr/lib/postfix/sbin/master') |
|
|
| |
| if not data['postfix']: |
| data['postfix'] = public.process_exists('master', '/usr/sbin/postfix') or public.process_exists('master', |
| '/usr/lib/postfix/sbin/master') or public.process_exists( |
| 'master', '/usr/libexec/postfix/master') |
|
|
| data['recipient_blacklist'] = self._recipient_blacklist_status() |
| |
| return self.return_msg(data) |
|
|
| def get_mail_log(self, args): |
| path = '/var/log/maillog' |
| if "ubuntu" in self.sys_v: |
| path = '/var/log/mail.log' |
| if not os.path.exists(path): return {'log': '文件不存在'} |
| text = public.GetNumLines(path, 500) |
| return self.return_msg({'log': text}) |
|
|
| |
| def M(self, table_name): |
| import db |
| sql = db.Sql() |
| sql._Sql__DB_FILE = '/www/vmail/postfixadmin.db' |
| sql._Sql__encrypt_keys = [] |
| return sql.table(table_name) |
|
|
| |
| def MD(self, table_name, db_key): |
| if db_key not in self.db_files: |
| raise ValueError(f"未知的数据库键: {db_key}") |
| import db |
| sql = db.Sql() |
| sql._Sql__DB_FILE = self.db_files[db_key] |
| sql._Sql__encrypt_keys = [] |
| return sql.table(table_name) |
|
|
| def flush_domain_record(self, args): |
| ''' |
| 手动刷新域名记录 |
| domain all/specify.com |
| :param args: |
| :return: |
| ''' |
| if args.domain == 'all': |
| data_list = self.M('domain').order('created desc').field('domain,a_record,created,active').select() |
| |
| for item in data_list: |
| try: |
| if os.path.exists("/usr/bin/rspamd"): |
| self.set_rspamd_dkim_key(item['domain']) |
| if os.path.exists("/usr/sbin/opendkim"): |
| self._gen_dkim_key(item['domain']) |
|
|
| |
| |
| |
| except: |
| return self.return_msg(public.returnMsg(False, '请检查Rspamd服务器是否已经启动!')) |
| self._gevent_jobs(item['domain'], item['a_record']) |
| else: |
| try: |
| if os.path.exists("/usr/bin/rspamd"): |
| self.set_rspamd_dkim_key(args.domain) |
| if os.path.exists("/usr/sbin/opendkim"): |
| self._gen_dkim_key(args.domain) |
| except: |
| return self.return_msg(public.returnMsg(False, '请检查Rspamd服务器是否已经启动!')) |
| try: |
| self._gevent_jobs(args.domain, None) |
| except: |
| public.print_log('error:{}'.format(str(public.get_error_info()))) |
| try: |
| public.writeFile(self._session_conf, json.dumps(self._session)) |
| return self.return_msg(public.returnMsg(True, '刷新成功!')) |
| except: |
| return self.return_msg(public.returnMsg(False, '刷新失败!')) |
|
|
| def get_record_in_cache(self, item): |
| try: |
| item['mx_status'] = self._session['{0}:{1}'.format(item['domain'], 'MX')]["status"] |
| item['spf_status'] = self._session['{0}:{1}'.format(item['domain'], 'TXT')]["status"] |
| item['dkim_status'] = self._session['{0}:{1}'.format("default._domainkey." + item['domain'], 'TXT')][ |
| "status"] |
| item['dmarc_status'] = self._session['{0}:{1}'.format("_dmarc." + item['domain'], 'TXT')]["status"] |
| item['a_status'] = self._session['{0}:{1}'.format(item['a_record'], 'A')]["status"] |
| if self._session['{0}:{1}'.format(item['domain'], 'PTR')]: |
| item['ptr_status'] = self._session['{0}:{1}'.format(item['domain'], 'PTR')]["status"] |
| except: |
| public.print_log(item) |
| self._gevent_jobs(item['domain'], item['a_record']) |
| self.get_record_in_cache(item) |
| return item |
|
|
| def get_domains(self, args): |
| ''' |
| 域名查询接口 |
| :param args: |
| :return: |
| ''' |
| from mailModel import multipleipModel |
| from sslModel.base import sslBase |
|
|
| multipleipModel_main = multipleipModel.main() |
|
|
| p = int(args.p) if 'p' in args else 1 |
| rows = int(args.size) if 'size' in args else 10 |
| callback = args.callback if 'callback' in args else '' |
| count = self.M('domain').count() |
| |
| if count == 0: |
| return self.return_msg(public.returnMsg(True, {'data': [], |
| 'page': "<div><span class='Pcurrent'>1</span><span class='Pcount'>Total 0</span></div>"})) |
|
|
| |
| page_data = public.get_page(count, p=p, rows=rows, callback=callback) |
|
|
| data_list = self.M('domain').order('created desc').limit(page_data['shift'] + ',' + page_data['row']).select() |
| if isinstance(data_list, str): |
| return public.returnMsg(False, data_list) |
| domain_ip = None |
| path = '/www/server/panel/plugin/mail_sys/domain_ip.json' |
| if os.path.exists(path): |
| domain_ip = public.readFile(path) |
| try: |
| domain_ip = json.loads(domain_ip) |
| except: |
| pass |
|
|
| blcheck_count = f'/www/server/panel/plugin/mail_sys/data/blcheck.json' |
|
|
| if os.path.exists(blcheck_count): |
| blcheck_ = public.readFile(blcheck_count) |
| try: |
| blcheck_ = json.loads(blcheck_) |
| except: |
| pass |
| else: |
| blcheck_ = {} |
|
|
| for item in data_list: |
| try: |
| if os.path.exists("/usr/bin/rspamd"): |
| self.set_rspamd_dkim_key(item['domain']) |
| if os.path.exists("/usr/sbin/opendkim"): |
| self._gen_dkim_key(item['domain']) |
| except: |
| public.print_log(public.get_error_info()) |
| return self.return_msg(public.returnMsg(False, '请检查rspamd服务状态是否正常')) |
| if not os.path.exists(self._session_conf): |
| self._gevent_jobs(item['domain'], item['a_record']) |
| item = self.get_record_in_cache(item) |
| else: |
| item = self.get_record_in_cache(item) |
| item['dkim_value'] = self._get_dkim_value(item['domain']) |
| item['dmarc_value'] = 'v=DMARC1;p=quarantine;rua=mailto:admin@{0}'.format(item['domain']) |
| item['mx_record'] = item['a_record'] |
| item['ssl_status'] = self._get_multiple_certificate_domain_status(item['domain']) |
| item['catch_all'], item['catch_type'], item['email'] = self._get_catchall_status(item['domain']) |
| item['ssl_info'] = self.get_ssl_info(item['domain']) |
|
|
| |
| |
| if domain_ip: |
| item['ip_address'] = domain_ip[item['domain']] if domain_ip.get(item['domain'], None) else {"ipv4": [], |
| "ipv6": []} |
| else: |
| item['ip_address'] = {"ipv4": [], "ipv6": []} |
|
|
| |
| item['domain_check_log'] = f"/www/server/panel/plugin/mail_sys/data/{item['a_record']}_blcheck.txt" |
| item['domain_black_count'] = blcheck_.get(item['a_record'], {}) |
| item['dns_id'] = 0 |
| item['dns_name'] = '' |
|
|
| |
| dns_data = public.M('ssl_domains').where("domain=?", (item['domain'],)).find() |
| if dns_data: |
| dns_dic = sslBase().get_dns_data(None).get(dns_data['dns_id'], {}) |
| if dns_dic: |
| item['dns_id'] = dns_data['dns_id'] |
| item['dns_name'] = dns_dic['dns_name'] |
| elif dns_data['dns_id'].find("cloud_id=") != -1: |
| item['dns_id'] = "宝塔DNS" |
| item['dns_name'] = "宝塔DNS" |
| bind = "@" + item['domain'] |
| ip_tags = multipleipModel_main.get_tag_bind(bind=bind) |
| now_tag = ip_tags[0] if ip_tags else '' |
| _tags = multipleipModel_main.get_ip_tags_api(args) |
| ip_dic = {i['tag']: i['ip'] for i in _tags['data']} |
| ip_rotate_conf = multipleipModel_main.get_ip_rotate_conf() |
| ip_rotate = ip_rotate_conf.get(item['domain']) |
| item['ip_tag'] = [] |
| item['ip_rotate'] = {"status": False} |
|
|
| if now_tag: |
| item['ip_tag'] = [{"tag": now_tag, "ip": ip_dic.get(now_tag, ''), "status": True}] |
|
|
| if ip_rotate: |
| item['ip_rotate'] = ip_rotate |
| for ip_tag in ip_rotate["tags"]: |
| if ip_tag == now_tag: |
| continue |
| item['ip_tag'].append({ |
| "tag": ip_tag, |
| "ip": ip_dic.get(ip_tag, ''), |
| "status": False |
| }) |
| else: |
| item['ip_rotate'] = {"status": False} |
|
|
| public.writeFile(self._session_conf, json.dumps(self._session)) |
| |
| return self.return_msg(public.returnMsg(True, { |
| 'data': data_list, |
| 'page': page_data['page'] |
| })) |
|
|
| def _get_domain_forward(self, domain): |
| address = '@' + domain.strip() |
| result = self.M('alias').where('domain=? AND (address=? or address=?) AND active = 1', (domain, address, '%'+address)).getField('goto') |
| if not result: |
| return '' |
| return result |
|
|
| def _gevent_jobs(self, domain, a_record): |
| from gevent import monkey |
| monkey.patch_all() |
| import gevent |
| gevent.joinall([ |
| gevent.spawn(self._check_mx, domain), |
| gevent.spawn(self._check_spf, domain), |
| gevent.spawn(self._check_dkim, domain), |
| gevent.spawn(self._check_dmarc, domain), |
| gevent.spawn(self._check_a, a_record), |
| |
| gevent.spawn(self._check_ptr, domain), |
| ]) |
| return True |
|
|
| def _build_dkim_sign_content(self, domain, dkim_path): |
| dkim_signing_conf = """#{domain}_DKIM_BEGIN |
| {domain} {{ |
| selectors [ |
| {{ |
| path: "{dkim_path}/default.private"; |
| selector: "default" |
| }} |
| ] |
| }} |
| #{domain}_DKIM_END |
| """.format(domain=domain, dkim_path=dkim_path) |
| return dkim_signing_conf |
|
|
| def _dkim_sign(self, domain, dkim_sign_content): |
| res = self.check_domain_in_rspamd_dkim_conf(domain) |
| if not res: |
| return False |
| sign_domain = '#BT_DOMAIN_DKIM_BEGIN{}#BT_DOMAIN_DKIM_END'.format( |
| res['sign_domain'].group(1) + dkim_sign_content) |
| sign_conf = re.sub(res['rep'], sign_domain, res['sign_conf']) |
| public.writeFile(res['sign_path'], sign_conf) |
| return True |
|
|
| def check_domain_in_rspamd_dkim_conf(self, domain): |
| sign_path = '/etc/rspamd/local.d/dkim_signing.conf' |
| sign_conf = public.readFile(sign_path) |
| if not sign_conf: |
| public.writeFile(sign_conf, "#BT_DOMAIN_DKIM_BEGIN\n#BT_DOMAIN_DKIM_END") |
| sign_conf = """ |
| domain { |
| #BT_DOMAIN_DKIM_BEGIN |
| #BT_DOMAIN_DKIM_END |
| } |
| """ |
| rep = '#BT_DOMAIN_DKIM_BEGIN((.|\n)+)#BT_DOMAIN_DKIM_END' |
| sign_domain = re.search(rep, sign_conf) |
| if not sign_domain: |
| return False |
| if domain in sign_domain.group(1): |
| return False |
| return {"rep": rep, "sign_domain": sign_domain, 'sign_conf': sign_conf, 'sign_path': sign_path} |
|
|
| def set_rspamd_dkim_key(self, domain): |
| dkim_path = '/www/server/dkim/{}'.format(domain) |
| if not dkim_path: |
| os.makedirs(dkim_path) |
| if not os.path.exists('{}/default.pub'.format(dkim_path)): |
| dkim_shell = """ |
| mkdir -p {dkim_path} |
| rspamadm dkim_keygen -s 'default' -b 1024 -d {domain} -k /www/server/dkim/{domain}/default.private > /www/server/dkim/{domain}/default.pub |
| chmod 755 -R /www/server/dkim/{domain} |
| """.format(dkim_path=dkim_path, domain=domain) |
| public.ExecShell(dkim_shell) |
| dkim_sign_content = self._build_dkim_sign_content(domain, dkim_path) |
| if self._dkim_sign(domain, dkim_sign_content): |
| public.ExecShell('systemctl reload rspamd') |
| return True |
|
|
| def _gen_dkim_key(self, domain): |
| if not os.path.exists('/usr/share/perl5/vendor_perl/Getopt/Long.pm'): |
| os.makedirs('/usr/share/perl5/vendor_perl/Getopt') |
| public.ExecShell( |
| 'wget -O /usr/share/perl5/vendor_perl/Getopt/Long.pm {}/install/plugin/mail_sys/Long.pm -T 10' |
| .format(public.get_url())) |
| if not os.path.exists('/etc/opendkim/keys/{0}/default.private'.format(domain)): |
| dkim_shell = ''' |
| mkdir /etc/opendkim/keys/{domain} |
| opendkim-genkey -D /etc/opendkim/keys/{domain}/ -d {domain} -s default -b 1024 |
| chown -R opendkim:opendkim /etc/opendkim/ |
| systemctl restart opendkim'''.format(domain=domain) |
| keytable = "default._domainkey.{domain} {domain}:default:/etc/opendkim/keys/{domain}/default.private".format( |
| domain=domain) |
| sigingtable = "*@{domain} default._domainkey.{domain}".format(domain=domain) |
| keytable_conf = public.readFile("/etc/opendkim/KeyTable") |
| sigingtable_conf = public.readFile("/etc/opendkim/SigningTable") |
| if keytable_conf: |
| if keytable not in keytable_conf: |
| keytable_conf = keytable_conf + keytable + "\n" |
| public.writeFile("/etc/opendkim/KeyTable", keytable_conf) |
| if sigingtable_conf: |
| if sigingtable not in sigingtable_conf: |
| sigingtable_conf = sigingtable_conf + sigingtable + "\n" |
| public.writeFile("/etc/opendkim/SigningTable", sigingtable_conf) |
| public.ExecShell(dkim_shell) |
|
|
| def _get_dkim_value(self, domain): |
| ''' |
| 解析/etc/opendkim/keys/domain/default.txt得到域名要设置的dkim记录值 |
| :param domain: |
| :return: |
| ''' |
| if not os.path.exists("/www/server/dkim/{}".format(domain)): |
| os.makedirs("/www/server/dkim/{}".format(domain)) |
| rspamd_pub_file = '/www/server/dkim/{}/default.pub'.format(domain) |
| opendkim_pub_file = '/etc/opendkim/keys/{0}/default.txt'.format(domain) |
| if os.path.exists(opendkim_pub_file) and not os.path.exists(rspamd_pub_file): |
| opendkim_pub = public.readFile(opendkim_pub_file) |
| public.writeFile(rspamd_pub_file, opendkim_pub) |
|
|
| rspamd_pri_file = '/www/server/dkim/{}/default.private'.format(domain) |
| opendkim_pri_file = '/etc/opendkim/keys/{}/default.private'.format(domain) |
| opendkim_pri = public.readFile(opendkim_pri_file) |
| public.writeFile(rspamd_pri_file, opendkim_pri) |
|
|
| if not os.path.exists(rspamd_pub_file): return '' |
| file_body = public.readFile(rspamd_pub_file).replace(' ', '').replace('\n', '').split('"') |
| value = file_body[1] + file_body[3] |
| return value |
|
|
| def _get_session(self): |
| session = public.readFile(self._session_conf) |
| if session: |
| session = json.loads(session) |
| else: |
| session = {} |
| return session |
|
|
| def _check_mx(self, domain): |
| ''' |
| 检测域名是否有mx记录 |
| :param domain: |
| :return: |
| ''' |
| a_record = self.M('domain').where('domain=?', domain).field('a_record').find()['a_record'] |
| key = '{0}:{1}'.format(domain, 'MX') |
| now = int(time.time()) |
| try: |
| value = "" |
| if key in self._session and self._session[key]["status"] != 0: |
| v_time = now - int(self._session[key]["v_time"]) |
| if v_time < self._check_time: |
| value = self._session[key]["value"] |
| if '' == value: |
| resolver = dns.resolver.Resolver() |
| resolver.timeout = 5 |
| resolver.lifetime = 10 |
| try: |
| result = resolver.query(domain, 'MX') |
| except: |
| result = resolver.resolve(domain, 'MX') |
| value = str(result[0].exchange).strip('.') |
| if not a_record: |
| a_record = value |
| self.M('domain').where('domain=?', domain).save('a_record', (a_record,)) |
| if value == a_record: |
| self._session[key] = {"status": 1, "v_time": now, "value": value} |
| return True |
| self._session[key] = {"status": 0, "v_time": now, "value": value} |
| return False |
| except: |
| public.print_log(public.get_error_info()) |
| self._session[key] = {"status": 0, "v_time": now, |
| "value": "None of DNS query names exist:{}".format(domain)} |
| return False |
|
|
| def _check_spf(self, domain): |
| ''' |
| 检测域名是否有spf记录 |
| :param domain: |
| :return: |
| ''' |
| key = '{0}:{1}'.format(domain, 'TXT') |
| now = int(time.time()) |
| try: |
| value = "" |
| if key in self._session and self._session[key]["status"] != 0: |
| v_time = now - int(self._session[key]["v_time"]) |
| if v_time < self._check_time: |
| value = self._session[key]["value"] |
| if '' == value: |
| resolver = dns.resolver.Resolver() |
| resolver.timeout = 5 |
| resolver.lifetime = 10 |
| try: |
| result = resolver.query(domain, 'TXT') |
| except: |
| result = resolver.resolve(domain, 'TXT') |
| for i in result.response.answer: |
| for j in i.items: |
| value += str(j).strip() |
| if 'v=spf1' in value.lower(): |
| self._session[key] = {"status": 1, "v_time": now, "value": value} |
| return True |
| self._session[key] = {"status": 0, "v_time": now, "value": value} |
| return False |
| except: |
| public.print_log(public.get_error_info()) |
|
|
| self._session[key] = {"status": 0, "v_time": now, "value": "None of DNS query spf exist:{}".format(domain)} |
| return False |
|
|
| def _check_dkim(self, domain): |
| ''' |
| 检测域名是否有dkim记录 |
| :param domain: |
| :return: |
| ''' |
| origin_domain = domain |
| domain = 'default._domainkey.{0}'.format(domain) |
| key = '{0}:{1}'.format(domain, 'TXT') |
| now = int(time.time()) |
| try: |
| value = "" |
| if key in self._session and self._session[key]["status"] != 0: |
| v_time = now - int(self._session[key]["v_time"]) |
| if v_time < self._check_time: |
| value = self._session[key]["value"] |
| if '' == value: |
| |
| resolver = dns.resolver.Resolver() |
| resolver.timeout = 5 |
| resolver.lifetime = 10 |
| try: |
| result = resolver.query(domain, 'TXT') |
| except: |
| result = resolver.resolve(domain, 'TXT') |
| for i in result.response.answer: |
| for j in i.items: |
| value += str(j).strip() |
| new_v = self._get_dkim_value(origin_domain) |
| if new_v and new_v in value: |
| self._session[key] = {"status": 1, "v_time": now, "value": value} |
| return True |
| self._session[key] = {"status": 0, "v_time": now, "value": value} |
| return False |
| except: |
| public.print_log(public.get_error_info()) |
| self._session[key] = {"status": 0, "v_time": now, |
| "value": "None of DNS query names exist:{}".format(domain)} |
| return False |
|
|
| def _check_dmarc(self, domain): |
| ''' |
| 检测域名是否有dmarc记录 |
| :param domain: |
| :return: |
| ''' |
| domain = '_dmarc.{0}'.format(domain) |
| key = '{0}:{1}'.format(domain, 'TXT') |
| now = int(time.time()) |
| try: |
| value = "" |
| if key in self._session and self._session[key]["status"] != 0: |
| v_time = now - int(self._session[key]["v_time"]) |
| if v_time < self._check_time: |
| value = self._session[key]["value"] |
| if '' == value: |
| |
| resolver = dns.resolver.Resolver() |
| resolver.timeout = 5 |
| resolver.lifetime = 10 |
| try: |
| result = resolver.query(domain, 'TXT') |
| except: |
| result = resolver.resolve(domain, 'TXT') |
| for i in result.response.answer: |
| for j in i.items: |
| value += str(j).strip() |
| if 'v=dmarc1' in value.lower(): |
| self._session[key] = {"status": 1, "v_time": now, "value": value} |
| return True |
| self._session[key] = {"status": 0, "v_time": now, "value": value} |
| return False |
| except: |
| public.print_log(public.get_error_info()) |
| self._session[key] = {"status": 0, "v_time": now, |
| "value": "None of DNS query names exist:{}".format(domain)} |
| return False |
|
|
| def _check_ptr(self, domain): |
| ''' |
| 检测IP地址是否有PTR记录 |
| :param ip_address: IP地址字符串 |
| :return: bool |
| ''' |
|
|
| ip_addresses = self._get_all_ip() |
| ip_addresses = [ip for ip in ip_addresses if ip != '127.0.0.1'] |
| |
| key = '{0}:{1}'.format(domain, 'PTR') |
|
|
| now = int(time.time()) |
|
|
| try: |
| value = "" |
| if key in self._session and self._session[key]["status"] != 0: |
| v_time = now - int(self._session[key]["v_time"]) |
| if v_time < self._check_time2: |
| value = self._session[key]["value"] |
| keys = self._session[key]["keys"] |
| values = self._session[key]["values"] |
|
|
| if value == "": |
| found_ptr_record = False |
| result = None |
| ptr_addr = None |
| for ip_address in ip_addresses: |
| if ':' in ip_address: |
| reverse_domain = self._ipv6_to_ptr(ip_address) |
| else: |
| reverse_domain = '.'.join(reversed(ip_address.split('.'))) + '.in-addr.arpa' |
|
|
| resolver = dns.resolver.Resolver() |
| resolver.timeout = 1 |
| resolver.lifetime = 3 |
| try: |
| |
| |
| result = resolver.query(reverse_domain, 'PTR') |
| found_ptr_record = True |
| ptr_addr = reverse_domain |
| |
| break |
| except: |
| continue |
| |
| if found_ptr_record: |
| values = [str(rdata.target).rstrip('.') for rdata in result] |
| |
| for i in values: |
| |
| if i.endswith(domain): |
| self._session[key] = {"status": 1, "v_time": now, "value": i, "key": ptr_addr, |
| "values": values} |
| return True |
| else: |
| continue |
| self._session[key] = {"status": 0, "v_time": now, |
| "value": "No matching PTR record:{}".format(values), "key": ptr_addr, |
| "values": values} |
| return False |
| else: |
| self._session[key] = {"status": 0, "v_time": now, |
| "value": "None of DNS query PTR exist:{}".format(domain), "key": ptr_addr, |
| "values": []} |
| return False |
| if domain in value.lower() or value.lower().endswith(domain): |
| self._session[key] = {"status": 1, "v_time": now, "value": value, "key": keys, "values": values} |
| return True |
|
|
| except: |
| public.print_log(public.get_error_info()) |
|
|
| self._session[key] = {"status": 0, "v_time": now, "value": "None of DNS query PTR exist:{}".format(domain)} |
| return False |
|
|
| def _ipv6_to_ptr(self, ipv6_address): |
| parts = ipv6_address.split(':') |
| normalized_parts = [part.zfill(4) for part in parts] |
| |
| normalized_address = ''.join(normalized_parts) |
| |
| reversed_address = normalized_address[::-1] |
| |
| ptr_address_parts = list(reversed_address) |
| ptr_address = '.'.join(ptr_address_parts) |
| ptr_address += '.ip6.arpa' |
| |
|
|
| return ptr_address |
|
|
| def get_mx_txt_cache(self, args): |
| session = self._get_session() |
| if 'domain' not in args: |
| return self.return_msg(public.returnMsg(False, '请传入域名')) |
| domain = args.domain |
|
|
| mx_key = '{0}:{1}'.format(domain, 'MX') |
| spf_key = '{0}:{1}'.format(domain, 'TXT') |
| dkim_key = '{0}:{1}'.format('default._domainkey.{0}'.format(domain), 'TXT') |
| dmarc_key = '{0}:{1}'.format('_dmarc.{0}'.format(domain), 'TXT') |
|
|
| mx_value = session[mx_key] if mx_key in session else '' |
| spf_value = session[spf_key] if spf_key in session else '' |
| dkim_value = session[dkim_key] if dkim_key in session else '' |
| dmarc_value = session[dmarc_key] if dmarc_key in session else '' |
|
|
| return self.return_msg({ |
| 'mx': mx_value, |
| 'spf': spf_value, |
| 'dkim': dkim_value, |
| 'dmarc': dmarc_value |
| }) |
|
|
| def delete_mx_txt_cache(self, args): |
| session = self._get_session() |
| if 'domain' not in args: |
| return self.return_msg(public.returnMsg(False, '请传入域名')) |
| domain = args.domain |
|
|
| mx_key = '{0}:{1}'.format(domain, 'MX') |
| spf_key = '{0}:{1}'.format(domain, 'TXT') |
| dkim_key = '{0}:{1}'.format('default._domainkey.{0}'.format(domain), 'TXT') |
| dmarc_key = '{0}:{1}'.format('_dmarc.{0}'.format(domain), 'TXT') |
|
|
| if mx_key in session: del (session[mx_key]) |
| if spf_key in session: del (session[spf_key]) |
| if dkim_key in session: del (session[dkim_key]) |
| if dmarc_key in session: del (session[dmarc_key]) |
| public.writeFile(self._session_conf, json.dumps(session)) |
|
|
| return self.return_msg(public.returnMsg(True, |
| '刷新域名({})在session中的缓存记录成功'.format(domain))) |
|
|
| def add_domain(self, args): |
| ''' |
| 域名增加接口 |
| :param args: |
| :return: |
| ''' |
| if 'domain' not in args: |
| return self.return_msg(public.returnMsg(False, '请传入域名')) |
| domain = args.domain |
| a_record = args.a_record |
| if not a_record.endswith(domain): |
| return self.return_msg(public.returnMsg(False, 'A记录 [{}] 不属于该域名'.format(a_record))) |
| if not self._check_a(a_record): |
| return self.return_msg(public.returnMsg( |
| False, 'A记录解析失败 <br>域名:{}<br>IP :{}'.format( |
| a_record, self._session['{}:A'.format(a_record)]['value']))) |
|
|
| if self.M('domain').where('domain=?', domain).count() > 0: |
| return self.return_msg(public.returnMsg(False, '该域名已存在')) |
|
|
| cur_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
| try: |
| self.M('domain').add('domain,a_record,created', (domain, a_record, cur_time)) |
| except: |
| return self.return_msg(public.returnMsg( |
| False, '邮局没有初始化成功!<br>' |
| '请尝试重新初始化,<br>' |
| '如果以下端口没访问将无法初始化 <br>port 25 [outbound direction]<br> ' |
| '你可以尝试执行以下命令测试端口是否开启:<br><br> [ telnet gmail-smtp-in.l.google.com 25 ] <br> ' |
| )) |
| errip = [] |
| |
| if 'ips' in args: |
| data = {domain: {"ipv4": [], "ipv6": []}} |
| ips = args.ips |
| |
| ip_list = ips.split(';') |
| |
| for ip in ip_list: |
| if public.is_ipv4(ip): |
| data[domain]["ipv4"].append(ip) |
| elif public.is_ipv6(ip): |
| data[domain]["ipv6"].append(ip) |
| else: |
| errip.append(ip) |
|
|
| |
| path = '/www/server/panel/plugin/mail_sys/domain_ip.json' |
|
|
| if not os.path.exists(path): |
| public.writeFile(path, json.dumps(data)) |
| else: |
| rdata = public.readFile(path) |
| try: |
| rdata = json.loads(rdata) |
| except: |
| pass |
| rdata.update(data) |
| public.writeFile(path, json.dumps(rdata)) |
| |
| dns_id = 0 |
| if "dns_id" in args: |
| dns_id = args.dns_id |
| |
| from sslModel.base import sslBase |
| root_domain, _, _ = sslBase().extract_zone(domain) |
| |
| dns_data = public.M('ssl_domains').where("domain=?", (root_domain,)).find() |
| if not dns_data: |
| public.M('ssl_domains').add('domain,dns_id,type_id,endtime,ps', (domain, dns_id, 0, 0, '')) |
| |
| if 'auto_create_record' in args and args.auto_create_record: |
| self.auto_create_dns_record(args) |
|
|
| |
| if not os.path.exists('/www/vmail/{0}'.format(domain)): |
| os.makedirs('/www/vmail/{0}'.format(domain)) |
| public.ExecShell('chown -R vmail:mail /www/vmail/{0}'.format(domain)) |
| if len(errip) > 0: |
| return self.return_msg(public.returnMsg(True, '新增域名[{}]成功! ip err:{}'.format(domain, errip))) |
| return self.return_msg(public.returnMsg(True, '新增域名[{0}]成功!'.format(domain))) |
|
|
| def edit_domain_record(self, args): |
| if 'domain' not in args: |
| return public.returnMsg(False, '请输入域名') |
| domain = args.domain |
| a_record = args.a_record |
| if self.M('domain').where('domain=?', domain).count() == 0: |
| return self.return_msg(public.returnMsg(False, '该域名不存在')) |
| self.M('domain').where('domain=?', domain).save('a_record', (a_record,)) |
| return self.return_msg(public.returnMsg(True, '修改域名[{0}]A记录成功!'.format(domain))) |
|
|
| def delete_domain(self, args): |
| ''' |
| 域名删除接口 |
| :param args: |
| :return: |
| ''' |
| if 'domain' not in args: |
| return self.return_msg(public.returnMsg(False, '请传入域名')) |
| domain = args.domain |
|
|
| |
| domain_info = self.M('domain').where('domain=?', (domain,)).find() |
| self.M('domain').where('domain=?', (domain,)).delete() |
|
|
| |
| self.M('mailbox').where('domain=?', (domain,)).delete() |
| self.delete_mx_txt_cache(args) |
|
|
| |
| self._deledte_catchall(domain) |
|
|
| |
| domain_check_log = f'/www/server/panel/plugin/mail_sys/data/{domain_info["a_record"]}_blcheck.txt' |
| if os.path.exists(domain_check_log): |
| os.remove(domain_check_log) |
| from mailModel import multipleipModel |
| multipleipModel_main = multipleipModel.main() |
| bind = "@{}".format(domain) |
| multipleipModel_main.del_bind_ip_tag(bind) |
| multipleipModel_main.del_ip_rotate_conf(bind) |
|
|
| |
| public.ExecShell('rm -rf /www/vmail/{0}'.format(domain)) |
| public.ExecShell('systemctl restart postfix') |
| return self.return_msg(public.returnMsg(True, '删除域成功! ({0})'.format(domain))) |
|
|
| def create_mail_box(self, user, passwd): |
| try: |
| import imaplib |
| conn = imaplib.IMAP4(port=143, host='127.0.0.1') |
| conn.login(user, passwd) |
| conn.select('Junk') |
| conn.select('Trash') |
| conn.select('Drafts') |
| conn.logout() |
| conn.close() |
| return True |
| except: |
| return False |
|
|
| def get_mailboxs1(self, args): |
| ''' |
| 邮箱用户查询接口 |
| :param args: |
| :return: |
| ''' |
| p = int(args.p) if 'p' in args else 1 |
| rows = int(args.size) if 'size' in args else 12 |
| callback = args.callback if 'callback' in args else '' |
| if 'domain' in args and args.domain != "": |
| domain = args.domain |
| count = self.M('mailbox').where('domain=?', domain).count() |
| |
| page_data = public.get_page(count, p, rows, callback) |
| |
| data_list = self.M('mailbox').order('created desc').limit( |
| page_data['shift'] + ',' + page_data['row']).where('domain=?', domain).field( |
| 'full_name,username,quota,created,modified,active,is_admin,password_encode,domain').select() |
| mx = self._check_mx_domain(domain) |
| for i in data_list: |
| i['password'] = self._decode(i['password_encode']) |
| del i['password_encode'] |
| i['mx'] = mx |
| |
| return self.return_msg({'data': data_list, 'page': page_data['page']}) |
| else: |
| count = self.M('mailbox').count() |
| |
| page_data = public.get_page(count, p, rows, callback) |
| |
| domains_mx = {} |
| domains = self.get_domain_name(None)["data"] |
| for i in domains: |
| mx = self._check_mx_domain(i) |
| domains_mx[i] = mx |
|
|
| |
| data_list = self.M('mailbox').order('created desc').limit( |
| page_data['shift'] + ',' + page_data['row']).field( |
| 'full_name,username,quota,created,modified,active,is_admin,password_encode,domain').select() |
| for i in data_list: |
| try: |
| i['password'] = self._decode(i['password_encode']) |
| del i['password_encode'] |
| |
| i['mx'] = domains_mx[i['domain']] |
| except: |
| pass |
|
|
| |
| return self.return_msg({'data': data_list, 'page': page_data['page']}) |
|
|
| def get_mailboxs(self, args): |
| ''' |
| 邮箱用户查询接口 |
| :param args: |
| :return: |
| ''' |
| p = int(args.p) if 'p' in args else 1 |
| rows = int(args.size) if 'size' in args else 12 |
| callback = args.callback if 'callback' in args else '' |
| if "search" in args and args.search != "": |
| where_str = "username LIKE ?" |
| where_args = (f"%{args.search.strip()}%",) |
| else: |
| where_str = "" |
| where_args = () |
| if 'domain' in args and args.domain != "": |
| domain = args.domain |
| if where_str and where_args: |
| where_str = "domain=? AND username LIKE?" |
| where_args = (domain, f"%{args.search.strip()}%") |
| else: |
| where_str = "domain=?" |
| where_args = (domain,) |
| with self.M('mailbox') as obj_mailbox: |
| count = obj_mailbox.where(where_str, where_args).count() |
| |
| page_data = public.get_page(count, p, rows, callback) |
| |
| with self.M('mailbox') as obj_mailbox: |
| data_list = obj_mailbox.order('created desc').limit( |
| page_data['shift'] + ',' + page_data['row']).where(where_str, where_args).field( |
| 'full_name,username,quota,created,modified,active,is_admin,password_encode,domain' |
| ).select() |
| mx = self._check_mx_domain(domain) |
| for i in data_list: |
| i['password'] = self._decode(i['password_encode']) |
| del i['password_encode'] |
| i['mx'] = mx |
| |
| return self.return_msg({'data': data_list, 'page': page_data['page']}) |
| else: |
| with self.M('mailbox') as obj_mailbox: |
| count = obj_mailbox.where(where_str, where_args).count() |
| |
| page_data = public.get_page(count, p, rows, callback) |
| |
| domains_mx = {} |
| domains = self.get_domain_name(None)["data"] |
| for i in domains: |
| mx = self._check_mx_domain(i) |
| domains_mx[i] = mx |
| |
| with self.M('mailbox') as obj_mailbox: |
| data_list = obj_mailbox.order('created desc').limit( |
| page_data['shift'] + ',' + page_data['row']).field( |
| 'full_name,username,quota,created,modified,active,is_admin,password_encode,domain' |
| ).where(where_str, where_args).select() |
| for i in data_list: |
| try: |
| i['password'] = self._decode(i['password_encode']) |
| del i['password_encode'] |
| |
| i['mx'] = domains_mx[i['domain']] |
| except: |
| pass |
|
|
| |
| return self.return_msg({'data': data_list, 'page': page_data['page']}) |
|
|
| def _check_mx_domain(self, domain): |
| ''' |
| 查询域名的mx |
| :param args: |
| :return: |
| ''' |
| key = '{0}:{1}'.format(domain, 'MX') |
| session = public.readFile('/www/server/panel/plugin/mail_sys/session.json') |
| if session: |
| session = json.loads(session) |
| else: |
| return '' |
|
|
| if session[key]['status']: |
| mx = session[key]['value'] |
| return mx |
| return '' |
|
|
| def get_all_user(self, args): |
| if 'domain' in args: |
| data_list = self.M('mailbox').where('domain=? AND active=?', (args.domain, 1)).field( |
| 'full_name,username,quota,created,modified,active,is_admin,domain').select() |
| else: |
| data_list = self.M('mailbox').where('active=?', 1).field( |
| 'full_name,username,quota,created,modified,active,is_admin,domain').select() |
| return self.return_msg(data_list) |
|
|
| |
| def _encode(self, data): |
| str2 = data.strip() |
| if sys.version_info[0] == 2: |
| b64_data = base64.b64encode(str2) |
| else: |
| b64_data = base64.b64encode(str2.encode('utf-8')) |
| return binascii.hexlify(b64_data).decode() |
|
|
| |
| def _decode(self, data): |
| b64_data = binascii.unhexlify(data.strip()) |
| return base64.b64decode(b64_data).decode() |
|
|
| |
| def _check_passwd(self, password): |
| return True if re.search(r"^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).*$", password) and len(password) >= 8 else False |
|
|
| def _check_email_address(self, email_address): |
| return True if re.match(r"^\w+([.-]?\w+)*@.*", email_address) else False |
|
|
| |
| def _generate_crypt_passwd(self, password): |
| if sys.version_info[0] == 2: |
| shell_str = 'doveadm pw -s MD5-CRYPT -p {0}'.format(password) |
| return public.ExecShell(shell_str)[0][11:].strip() |
| else: |
| import crypt |
| return crypt.crypt(password, crypt.mksalt(crypt.METHOD_MD5)) |
|
|
| |
| def __create_mail_box_mulitiple(self, info, args): |
| create_successfully = {} |
| create_failed = {} |
| |
| for data in info: |
| if not data: |
| continue |
| try: |
| args.quota = '{} {}'.format(data['quota'], data['unit']) |
| args.username = data['username'] |
| args.password = data['password'] |
| args.full_name = data['full_name'] |
| args.is_admin = 0 |
| result = self.add_mailbox(args) |
| if result['status']: |
| create_successfully[data['username']] = result['msg'] |
| continue |
| |
| create_failed[data['username']] = result['msg'] |
| except: |
| create_failed[data['username']] = "create error" |
| |
| |
| return {'status': True, 'msg': "Create the mailbox [ {} ] successfully".format(','.join(create_successfully)), |
| 'error': create_failed, |
| 'success': create_successfully} |
|
|
| |
| def add_mailbox_multiple(self, args): |
| ''' |
| @name 批量创建网站 |
| @author zhwen<2020-11-26> |
| @param create_type txt txt格式为 “Name|Address|Password|MailBox space|GB” 每个网站一行 |
| "support|support|Password|5|GB" |
| @param content "["support|support|Password|5|GB"]" |
| ''' |
| key = ['full_name', 'username', 'password', 'quota', 'unit'] |
| info = [dict(zip(key, i)) for i in |
| [i.strip().split('|') for i in json.loads(args.content)]] |
| if not info: |
| return self.return_msg(public.returnMsg(False, '参数为空,密码强度不足(需要包含大小写字母和数字,长度不小于8)')) |
| res = self.__create_mail_box_mulitiple(info, args) |
| |
| |
| return self.return_msg(res) |
|
|
| def add_mailbox(self, args): |
| ''' |
| 新增邮箱用户 |
| :param args: |
| :return: |
| ''' |
| if 'username' not in args: |
| return self.return_msg(public.returnMsg(False, '请传入账号名')) |
| if not self._check_passwd(args.password): |
| return self.return_msg(public.returnMsg(False, '密码强度不够(需要包括大小写字母和数字并且长度不小于8)')) |
| username = args.username |
| |
| |
| if not username.islower(): |
| return self.return_msg(public.returnMsg(False, '邮箱地址不能有大写字母!')) |
| is_admin = args.is_admin if 'is_admin' in args else 0 |
|
|
| active = 1 |
| if 'active' in args and args.active == "0": |
| active = 0 |
| local_part, domain = username.split('@') |
| |
| with self.M('mailbox') as obj_mailbox: |
| user_count = obj_mailbox.where('domain=?', (domain,)).count() |
| count = obj_mailbox.where('username=?', (username,)).count() |
|
|
| if count > 0: |
| return self.return_msg(public.returnMsg(False, '邮箱已存在')) |
|
|
| with self.M('domain') as obj_domain: |
| domaincount = obj_domain.where('domain=?', (domain,)).getField("mailboxes") |
|
|
| if user_count + 1 > domaincount: |
| return self.return_msg(public.returnMsg(False, '{}的邮箱数量已达上限{}'.format(domain, |
| domaincount))) |
|
|
| password_encrypt = self._generate_crypt_passwd(args.password) |
| password_encode = self._encode(args.password) |
|
|
| domain_list = self.get_domain_name(None)["data"] |
| if domain not in domain_list: |
| return self.return_msg(public.returnMsg(False, '域名列表不存在域名{}'.format(domain))) |
| num, unit = args.quota.split() |
| if unit == 'GB': |
| quota = float(num) * 1024 * 1024 * 1024 |
| else: |
| quota = float(num) * 1024 * 1024 |
|
|
| cur_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
|
|
| is_insert = True |
| while is_insert: |
| try: |
| with self.M('mailbox') as obj_mailbox: |
| res = obj_mailbox.add( |
| 'full_name,is_admin,username,password,password_encode,maildir,quota,local_part,domain,created,modified,active', |
| (args.full_name, is_admin, username, password_encrypt, password_encode, args.username + '/', |
| quota, |
| local_part, domain, cur_time, cur_time, active)) |
| if isinstance(res, str): |
| if 'error' in res: |
| |
| continue |
|
|
| is_insert = False |
| |
| except: |
| time.sleep(0.01) |
| continue |
|
|
| |
| user_path = '/www/vmail/{0}/{1}'.format(domain, local_part) |
| os.makedirs(user_path) |
| os.makedirs(user_path + '/tmp') |
| os.makedirs(user_path + '/new') |
| os.makedirs(user_path + '/cur') |
|
|
| |
| dir_path = '/www/vmail/{0}/{1}/.Sent/cur'.format(domain, local_part) |
| if not os.path.isdir(dir_path): |
| os.makedirs(dir_path) |
|
|
| os.system('chown -R vmail:mail /www/vmail/{0}/{1}'.format(domain, local_part)) |
| |
| |
|
|
| return self.return_msg(public.returnMsg(True, '增加邮箱用户[{0}]成功!'.format(username))) |
|
|
| def update_mailbox(self, args): |
| ''' |
| 邮箱用户修改接口 |
| :param args: |
| :return: |
| ''' |
| num, unit = args.quota.split() |
| if unit == 'GB': |
| quota = float(num) * 1024 * 1024 * 1024 |
| else: |
| quota = float(num) * 1024 * 1024 |
| cur_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
|
|
| if 'password' in args and args.password != '': |
| if not self._check_passwd(args.password): |
| return public.returnMsg(False, '密码强度不够(需要包括大小写字母和数字并且长度不小于8)') |
| |
| |
| password_encrypt = self._generate_crypt_passwd(args.password) |
| password_encode = self._encode(args.password) |
| self.M('mailbox').where('username=?', args.username).save( |
| 'password,password_encode,full_name,quota,modified,active,is_admin', |
| (password_encrypt, password_encode, args.full_name, quota, cur_time, args.active, args.is_admin)) |
| else: |
| self.M('mailbox').where('username=?', args.username).save('full_name,quota,modified,active,is_admin', ( |
| args.full_name, quota, cur_time, args.active, args.is_admin)) |
| return self.return_msg(public.returnMsg(True, '编辑邮箱用户成功! ({0})'.format(args.username))) |
|
|
| def delete_mailbox(self, args): |
| ''' |
| 删除邮箱用户 |
| :param args: |
| :return: |
| ''' |
| if 'username' not in args: |
| return self.return_msg(public.returnMsg(False, '请传入账号名')) |
| username = args.username |
| local_part, domain = username.split('@') |
| res = self.M('mailbox').where('username=?', username).count() |
| if not res: |
| return self.return_msg(public.returnMsg(False, "删除失败!")) |
| self.M('mailbox').where('username=?', username).delete() |
|
|
| |
| if os.path.exists('/www/vmail/{0}/{1}'.format(domain, local_part)): |
| public.ExecShell('rm -rf /www/vmail/{0}/{1}'.format(domain, local_part)) |
| return self.return_msg(public.returnMsg(True, '删除邮箱用户成功! ({0})'.format(username))) |
|
|
| def send_mail(self, args): |
| |
| service_status = self.get_service_status(args)["data"] |
| if not service_status['postfix']: |
| return self.return_msg(public.returnMsg(False, '无法发送邮件, 错误原因: 部分服务未启动,请查看服务状态')) |
| if not self._check_smtp_port(): |
| return self.return_msg(public.returnMsg( |
| False, '部分云厂商(如:阿里云,腾讯云)默认关闭25端口,需联系厂商开通25端口后才能正常使用邮局服务')) |
|
|
| |
| |
| |
| |
| |
|
|
| |
| mail_from = args.mail_from |
| data = self.M('mailbox').where('username=?', mail_from).field('password_encode,full_name').find() |
| password = self._decode(data['password_encode']) |
| |
| mail_to = json.loads(args.mail_to) if 'mail_to' in args else [] |
|
|
| |
| |
| |
| |
| |
| subject = args.subject |
| content = args.content |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| content = '<html>' + content + '</html>' |
|
|
| |
| files = json.loads(args.files) if 'files' in args else [] |
| |
| if not isinstance(mail_to, list): |
| return self.return_msg(public.returnMsg(False, '收件人不能解析成列表')) |
| if len(mail_to) == 0: |
| return self.return_msg(public.returnMsg(False, '收件人不能为空')) |
|
|
| try: |
|
|
| |
| send_mail_client = SendMail(mail_from, password, 'localhost') |
| |
| |
| send_mail_client.setMailInfo(data['full_name'], subject, content, files) |
| |
| _, domain = mail_from.split('@') |
| result = send_mail_client.sendMail(mail_to, domain, 1) |
| return self.return_msg(result) |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
| return self.return_msg(public.returnMsg(False, '发送邮件失败,错误原因[{0}]'.format(str(e)))) |
|
|
| |
| def send_mail_test(self, args): |
| |
| service_status = self.get_service_status(args)["data"] |
| if not service_status['postfix']: |
| return self.return_msg(public.returnMsg(False, '无法发送邮件,错误原因:某些服务未启动,请检查服务状态')) |
| |
| if not self._check_smtp_port(): |
| return self.return_msg(public.returnMsg(False, '一些云供应商(如阿里云、腾讯云)默认关闭端口25,您需要联系供应商打开端口25,然后才能正常使用邮局服务')) |
|
|
| |
| |
| |
| |
| |
| |
| from mailModel.bulkModel import main as SendMailBulk |
|
|
| return self.return_msg(SendMailBulk().send_mail_test(args)) |
|
|
| def _check(self, args): |
| if args['fun'] in ['send_mail_http']: |
| return self.return_msg(True) |
| else: |
| return self.return_msg(public.returnMsg(False, '接口不支持公共访问!')) |
|
|
| def send_mail_http(self, args): |
| service_status = self.get_service_status(args)["data"] |
| if not service_status['postfix']: |
| return self.return_msg(public.returnMsg(False, '无法发送邮件, 错误原因: 部分服务未启动,请查看服务状态')) |
| if not self._check_smtp_port(): |
| return self.return_msg(public.returnMsg( |
| False, '部分云厂商(如:阿里云,腾讯云)默认关闭25端口,需联系厂商开通25端口后才能正常使用邮局服务')) |
|
|
| mail_from = args.mail_from |
| password = args.password |
| mail_to = [item.strip() for item in args.mail_to.split(',')] |
| |
| |
| |
| |
| subject = args.subject |
| content = args.content |
|
|
| content = '<html>' + content + '</html>' |
| files = json.loads(args.files) if 'files' in args else [] |
|
|
| try: |
| data = self.M('mailbox').where('username=?', mail_from).field('full_name').find() |
| send_mail_client = SendMail(mail_from, password, 'localhost') |
| send_mail_client.setMailInfo(data['full_name'], subject, content, files) |
| _, domain = mail_from.split('@') |
| result = send_mail_client.sendMail(mail_to, domain, 1) |
| return self.return_msg(result) |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
| return self.return_msg(public.returnMsg(False, '发送邮件失败,错误原因[{0}]'.format(str(e)))) |
|
|
| |
| def get_encoding(self, file): |
| import chardet |
|
|
| try: |
| |
| with open(file, 'rb') as f: |
| data = f.read() |
| return chardet.detect(data)['encoding'] |
| except: |
| return 'ascii' |
|
|
| def get_mails(self, args): |
| import email |
| from mailModel import receive_mail |
| reload(receive_mail) |
|
|
| if 'username' not in args: |
| return self.return_msg(public.returnMsg(False, '请传入账号名')) |
| username = args.username |
| if '@' not in username: |
| return self.return_msg(public.returnMsg(False, '账号名不合法')) |
| local_part, domain = username.split('@') |
| if 'p' not in args: |
| args.p = 1 |
| if 'p=' in args.p: |
| args.p = args.p.replace('p=', '') |
|
|
| receive_mail_client = receive_mail.ReceiveMail() |
| mail_list = [] |
| try: |
| dir_path = '/www/vmail/{0}/{1}/cur'.format(domain, local_part) |
| if os.path.isdir(dir_path): |
| |
| new_path = '/www/vmail/{0}/{1}/new'.format(domain, local_part) |
| if os.path.isdir(new_path): |
| for file in os.listdir(new_path): |
| src = os.path.join(new_path, file) |
| dst = os.path.join(dir_path, file) |
| shutil.move(src, dst) |
| files = [] |
| for fname in os.listdir(dir_path): |
| mail_file = os.path.join(dir_path, fname) |
| if not os.path.exists(mail_file): continue |
| f_info = {} |
| f_info['name'] = fname |
| f_info['mtime'] = os.path.getmtime(mail_file) |
| |
| save_day = self.get_save_day(None)['data'] |
| if save_day > 0: |
| deltime = int(time.time()) - save_day * 86400 |
| if int(f_info['mtime']) < deltime: |
| os.remove(mail_file) |
| continue |
| files.append(f_info) |
| files = sorted(files, key=lambda x: x['mtime'], reverse=True) |
| page_data = public.get_page(len(files), int(args.p), 10) |
| |
| |
| pattern = r"href='(?:/v2)?/plugin.*?\?p=(\d+)'" |
| |
| page_data['page'] = re.sub(pattern, r"href='\1'", page_data['page']) |
|
|
| shift = int(page_data['shift']) |
| row = int(page_data['row']) |
| files = files[shift:shift + row] |
| for d in files: |
| mail_file = os.path.join(dir_path, d['name']) |
| try: |
| mailInfo = receive_mail_client.getMailInfo(public.readFile(mail_file)) |
| mailInfo['path'] = mail_file |
| mailInfo['text'] = public.readFile(mail_file) |
| mail_list.append(mailInfo) |
| except: |
| public.writeFile( |
| "{}/error.log".format(self.__setupPath), |
| public.get_error_info()) |
| continue |
| return self.return_msg({ |
| 'status': |
| True, |
| 'data': |
| mail_list, |
| 'page': |
| page_data['page'].replace( |
| '/plugin?action=a&name=mail_sys&s=get_mails&p=', '') |
| }) |
| else: |
| page_data = public.get_page(0, int(args.p), 10) |
| return self.return_msg({ |
| 'status': |
| True, |
| 'data': |
| mail_list, |
| 'page': |
| page_data['page'] |
| }) |
| except Exception as e: |
| print(public.get_error_info()) |
| return self.return_msg(public.returnMsg(False, '获取邮件失败,错误原因[{0}]'.format(str(e)))) |
|
|
| def delete_mail(self, args): |
| path = args.path |
| if not os.path.exists(path): |
| return self.return_msg(public.returnMsg(False, '邮件路径不存在')) |
| os.remove(path) |
| return self.return_msg(public.returnMsg(True, '删除邮件成功')) |
|
|
| def get_config(self, args): |
| from files import files |
|
|
| if args.service == 'postfix': |
| args.path = '/etc/postfix/main.cf' |
| elif args.service == 'dovecot': |
| args.path = '/etc/dovecot/dovecot.conf' |
| elif args.service == 'rspamd': |
| args.path = '/etc/rspamd/rspamd.conf' |
| elif args.service == 'opendkim': |
| args.path = '/etc/opendkim.conf' |
| else: |
| return self.return_msg(public.returnMsg(False, '服务名不正确')) |
|
|
| return self.return_msg(files().GetFileBody(args)) |
|
|
| def save_config(self, args): |
| from files import files |
|
|
| if args.service == 'postfix': |
| args.path = '/etc/postfix/main.cf' |
| elif args.service == 'dovecot': |
| args.path = '/etc/dovecot/dovecot.conf' |
| elif args.service == 'rspamd': |
| args.path = '/etc/rspamd/rspamd.conf' |
| elif args.service == 'opendkim': |
| args.path = '/etc/opendkim.conf' |
| else: |
| return self.return_msg(public.returnMsg(False, '服务名不正确')) |
| args.encoding = 'utf-8' |
|
|
| result = files().SaveFileBody(args) |
| if result['status']: |
| if args.service == 'postfix': |
| public.ExecShell('systemctl reload postfix') |
| elif args.service == 'dovecot': |
| public.ExecShell('systemctl reload dovecot') |
| elif args.service == 'rspamd': |
| public.ExecShell('systemctl reload rspamd') |
| elif args.service == 'opendkim': |
| public.ExecShell('systemctl reload opendkim') |
| return self.return_msg(result) |
|
|
| def service_admin(self, args): |
| service_name = args.service |
| if service_name.lower() not in [ |
| 'postfix', 'dovecot', 'rspamd', 'opendkim' |
| ]: |
| return self.return_msg(public.returnMsg(False, '服务名不正确')) |
| type = args.type |
| if type.lower() not in ['start', 'stop', 'restart', 'reload']: |
| return self.return_msg(public.returnMsg(False, '操作不正确')) |
|
|
| exec_str = 'systemctl {0} {1}'.format(type, service_name) |
| if type == 'reload': |
| if service_name == 'postfix': |
| exec_str = '/usr/sbin/postfix reload' |
| elif service_name == 'dovecot': |
| exec_str = '/usr/bin/doveadm reload' |
| elif service_name == 'rspamd': |
| exec_str = 'systemctl reload rspamd' |
| elif service_name == 'opendkim': |
| exec_str = 'systemctl reload opendkim' |
| if service_name == 'opendkim' and type in ('start', 'restart'): |
| exec_str = ''' |
| sed -i "s#/var/run/opendkim/opendkim.pid#/run/opendkim/opendkim.pid#" /etc/opendkim.conf |
| sed -i "s#/var/run/opendkim/opendkim.pid#/run/opendkim/opendkim.pid#" /etc/sysconfig/opendkim |
| sed -i "s#/var/run/opendkim/opendkim.pid#/run/opendkim/opendkim.pid#" /usr/lib/systemd/system/opendkim.service |
| systemctl daemon-reload |
| systemctl enable opendkim |
| systemctl restart opendkim |
| ''' |
|
|
| public.ExecShell(exec_str) |
| return self.return_msg(public.returnMsg(True, |
| '{0}执行{1}操作成功'.format(service_name, type))) |
|
|
| |
| def get_sent_mails(self, args): |
| import email |
| from mailModel import receive_mail |
| reload(receive_mail) |
|
|
| if 'username' not in args: |
| return self.return_msg(public.returnMsg(False, '请传入账号名')) |
| username = args.username |
| if '@' not in username: |
| return self.return_msg(public.returnMsg(False, '账号名不合法')) |
| local_part, domain = username.split('@') |
| if 'p' not in args: |
| args.p = 1 |
| if 'p=' in args.p: |
| args.p = args.p.replace('p=', '') |
|
|
| receive_mail_client = receive_mail.ReceiveMail() |
| mail_list = [] |
| try: |
| |
| dir_path = '/www/vmail/{0}/{1}/.Sent/cur'.format(domain, local_part) |
| if os.path.isdir(dir_path): |
| files = [] |
| for fname in os.listdir(dir_path): |
| mail_file = os.path.join(dir_path, fname) |
| if not os.path.exists(mail_file): continue |
| f_info = {} |
| f_info['name'] = fname |
| f_info['mtime'] = os.path.getmtime(mail_file) |
| save_day = self.get_save_day(None)["data"] |
| if save_day > 0: |
| deltime = int(time.time()) - save_day * 86400 |
| if int(f_info['mtime']) < deltime: |
| os.remove(mail_file) |
| continue |
| files.append(f_info) |
| files = sorted(files, key=lambda x: x['mtime'], reverse=True) |
| page_data = public.get_page(len(files), int(args.p), 10) |
| |
| |
| pattern = r"href='(?:/v2)?/plugin.*?\?p=(\d+)'" |
| |
| page_data['page'] = re.sub(pattern, r"href='\1'", page_data['page']) |
| shift = int(page_data['shift']) |
| row = int(page_data['row']) |
| files = files[shift:shift + row] |
| for d in files: |
| mail_file = os.path.join(dir_path, d['name']) |
| fp = open(mail_file, 'r') |
| try: |
| message = email.message_from_file(fp) |
| mailInfo = receive_mail_client.getMailInfo(public.readFile(mail_file)) |
| mailInfo['path'] = mail_file |
| mail_list.append(mailInfo) |
| except: |
| public.print_log(public.get_error_info()) |
| continue |
| return self.return_msg({ |
| 'status': |
| True, |
| 'data': |
| mail_list, |
| 'page': |
| page_data['page'] |
| }) |
| else: |
| page_data = public.get_page(0, int(args.p), 10) |
| return self.return_msg({ |
| 'status': |
| True, |
| 'data': |
| mail_list, |
| 'page': |
| page_data['page'] |
| }) |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
| return self.return_msg(public.returnMsg(False, |
| '获取已发送邮件失败,错误原因[{0}]'.format(str(e)))) |
|
|
| |
| def set_postfix_ssl(self, csrpath, keypath, act): |
| main_file = self.postfix_main_cf |
| master_file = "/etc/postfix/master.cf" |
| main_conf = public.readFile(main_file) |
| master_conf = public.readFile(master_file) |
| if act == "0": |
| csrpath = "/etc/pki/dovecot/certs/dovecot.pem" |
| keypath = "/etc/pki/dovecot/private/dovecot.pem" |
| master_rep = r"\n*\s*-o\s+smtpd_tls_auth_only=yes" |
| master_str = "\n# -o smtpd_tls_auth_only=yes" |
| master_rep1 = r"\n*\s*-o\s+smtpd_tls_wrappermode=yes" |
| master_str1 = "\n# -o smtpd_tls_wrappermode=yes" |
| else: |
| master_rep = r"\n*#\s*-o\s+smtpd_tls_auth_only=yes" |
| master_str = "\n -o smtpd_tls_auth_only=yes" |
| master_rep1 = r"\n*#\s*-o\s+smtpd_tls_wrappermode=yes" |
| master_str1 = "\n -o smtpd_tls_wrappermode=yes" |
|
|
| for i in [[main_conf, main_file], [master_conf, master_file]]: |
| if not i[0]: |
| return public.returnMsg(False, |
| "找不到postfix配置文件 {}".format(i[1])) |
| main_rep = r"smtpd_tls_cert_file\s*=\s*.+" |
| main_conf = re.sub(main_rep, "smtpd_tls_cert_file = {}".format(csrpath), main_conf) |
| main_rep = r"smtpd_tls_key_file\s*=\s*.+" |
| main_conf = re.sub(main_rep, "smtpd_tls_key_file = {}".format(keypath), main_conf) |
| public.writeFile(main_file, main_conf) |
| |
| master_conf = re.sub(master_rep, master_str, master_conf) |
| master_conf = re.sub(master_rep1, master_str1, master_conf) |
| public.writeFile(master_file, master_conf) |
|
|
| def get_dovecot_version(self, args=None): |
| data = public.ExecShell("dpkg -l|grep dovecot-core|awk -F':' '{print $2}'")[0] |
| if os.path.exists('/etc/redhat-release'): |
| data = public.ExecShell('rpm -qa | grep dovecot | grep -v pigeonhole')[0].split('-')[1] |
| return self.return_msg(data) |
|
|
| def set_dovecot_ssl(self, csrpath, keypath, act): |
| dovecot_version = self.get_dovecot_version()['data'] |
| ssl_file = "/etc/dovecot/conf.d/10-ssl.conf" |
| ssl_conf = public.readFile(ssl_file) |
| if not ssl_conf: |
| return public.returnMsg(False, |
| "找不到dovecot配置文件 {}".format(ssl_file)) |
| if act == "0": |
| csrpath = "/etc/pki/dovecot/certs/dovecot.pem" |
| keypath = "/etc/pki/dovecot/private/dovecot.pem" |
| ssl_rep = r"ssl_cert\s*=\s*<.+" |
| ssl_conf = re.sub(ssl_rep, "ssl_cert = <{}".format(csrpath), ssl_conf) |
| ssl_rep = r"ssl_key\s*=\s*<.+" |
| ssl_conf = re.sub(ssl_rep, "ssl_key = <{}".format(keypath), ssl_conf) |
| if dovecot_version.startswith('2.3'): |
| if act == '1': |
| if not os.path.exists('/etc/dovecot/dh.pem') or os.path.getsize('/etc/dovecot/dh.pem') < 300: |
| public.ExecShell('openssl dhparam 2048 > /etc/dovecot/dh.pem') |
| ssl_conf = ssl_conf + "\nssl_dh = </etc/dovecot/dh.pem" |
| else: |
| ssl_conf = re.sub(r'\nssl_dh = </etc/dovecot/dh.pem', '', ssl_conf) |
| os.remove('/etc/dovecot/dh.pem') |
| public.writeFile(ssl_file, ssl_conf) |
|
|
| |
| def set_ssl(self, args): |
| path = '{}/cert/'.format(self.__setupPath) |
| csrpath = path + "fullchain.pem" |
| keypath = path + "privkey.pem" |
| backup_cert = '/tmp/backup_cert_mail_sys' |
| if hasattr(args, "act") and args.act == "1": |
| if args.key.find('KEY') == -1: |
| return self.return_msg(public.returnMsg(False, '私钥错误,请检查!')) |
| if args.csr.find('CERTIFICATE') == -1: |
| return self.return_msg(public.returnMsg(False, '证书错误,请检查!')) |
| public.writeFile('/tmp/mail_cert.pl', str(args.csr)) |
| if not public.CheckCert('/tmp/mail_cert.pl'): |
| return self.return_msg(public.returnMsg(False, '证书错误,请以pem格式粘贴正确的证书!')) |
| if os.path.exists(backup_cert): shutil.rmtree(backup_cert) |
| if os.path.exists(path): shutil.move(path, backup_cert) |
| if os.path.exists(path): shutil.rmtree(path) |
|
|
| os.makedirs(path) |
| public.writeFile(keypath, args.key) |
| public.writeFile(csrpath, args.csr) |
| else: |
| if os.path.exists(csrpath): |
| os.remove(csrpath) |
| if os.path.exists(keypath): |
| os.remove(keypath) |
|
|
| |
| p_result = self.set_postfix_ssl(csrpath, keypath, args.act) |
| if p_result: return self.return_msg(p_result) |
| d_result = self.set_dovecot_ssl(csrpath, keypath, args.act) |
| if d_result: return self.return_msg(d_result) |
|
|
| import time |
| for i in ["dovecot", "postfix"]: |
| args.service = i |
| args.type = "restart" |
| self.service_admin(args) |
| time.sleep(1) |
| |
| if os.path.exists(backup_cert): shutil.rmtree(backup_cert) |
| return public.returnMsg(True, '设置成功') |
|
|
| |
| def get_ssl_status(self, args): |
| path = '{0}/cert/'.format(self.__setupPath) |
| csrpath = path + "fullchain.pem" |
| keypath = path + "privkey.pem" |
| if not (os.path.exists(csrpath) and os.path.exists(keypath)): |
| return self.return_msg(False) |
| main_file = self.postfix_main_cf |
| main_conf = public.readFile(main_file) |
| master_file = "/etc/postfix/master" |
| master_conf = public.readFile(master_file) |
| dovecot_ssl_file = "/etc/dovecot/conf.d/10-ssl.conf" |
| dovecot_ssl_conf = public.readFile(dovecot_ssl_file) |
| if main_conf: |
| if csrpath not in main_conf and keypath not in main_conf: |
| return self.return_msg(False) |
| if master_conf: |
| rep = r"\n*\s*-o\s+smtpd_sasl_auth_enable\s*=\s*yes" |
| if not re.search(rep, master_conf): |
| return self.return_msg(False) |
| if dovecot_ssl_conf: |
| if csrpath not in main_conf and keypath not in main_conf: |
| return self.return_msg(False) |
| return self.return_msg(True) |
|
|
| |
| def _get_all_ip(self): |
| |
|
|
| public_ip = self._get_pubilc_ip() |
| net_info = psutil.net_if_addrs() |
| addr = [] |
| for i in net_info.values(): |
| addr.append(i[0].address) |
| locataddr = public.readFile('/www/server/panel/data/iplist.txt') |
| if not locataddr: |
| locataddr = "" |
| ip_address = locataddr.strip() |
| if ip_address not in addr: |
| addr.append(ip_address) |
| if public_ip not in addr: |
| addr.append(public_ip) |
| return addr |
|
|
| def get_bcc(self, args): |
| forward = public.readFile(self._forward_conf) |
| if forward: |
| forward = json.loads(forward) |
| else: |
| forward = {"recipient": [], "sender": []} |
| |
| if forward['recipient']: |
| for d in forward['recipient']: |
| d.setdefault('active', 1) |
| if forward['sender']: |
| for d in forward['sender']: |
| d.setdefault('active', 1) |
| return self.return_msg(forward) |
|
|
| |
| def set_mail_bcc(self, args): |
| """ |
| type sender/recipien |
| user domain_name/email_address |
| forward_user email_address |
| domain domain |
| active active 0/1 默认1 开启 |
| :param args: |
| :return: |
| """ |
| |
| if not hasattr(args, 'active') or args.get('active/d', 1) == 1: |
| args.active = 1 |
| else: |
| args.active = 0 |
| |
| |
| args.domain = args.user.strip().split('@')[1] |
| data = self.get_bcc(args)['data'] |
| for d in data[args.type]: |
| if args.user == d["user"] and args.forward_user == d[ |
| "forward_user"]: |
| return self.return_msg(public.returnMsg(False, "已存在")) |
|
|
| |
| if args.active: |
| rep = r"^(?=^.{3,255}$)[a-zA-Z0-9\_\-][a-zA-Z0-9\_\-]{0,62}(\.[a-zA-Z0-9\_\-][a-zA-Z0-9\_\-]{0,62})+$" |
| if re.search(rep, args.user): |
| content = "\n@{} {}".format(args.user, args.forward_user) |
| else: |
| content = "\n{} {}".format(args.user, args.forward_user) |
| |
| bcc_file = "/etc/postfix/{}_bcc".format(args.type) |
| public.writeFile(bcc_file, content, "a+") |
|
|
| |
| data[args.type].append( |
| {"domain": args.domain, "user": args.user, "forward_user": args.forward_user, "active": args.active}) |
|
|
| public.writeFile(self._forward_conf, json.dumps(data)) |
| for i in ["/etc/postfix/sender_bcc", "/etc/postfix/recipient_bcc"]: |
| if not os.path.exists(i): |
| public.writeFile(i, "") |
| |
| |
|
|
| bcc_conf = '' |
| if not self.check_postfix_bcc('recipient_bcc_maps'): |
| bcc_conf += 'recipient_bcc_maps = hash:/etc/postfix/recipient_bcc\n' |
| if not self.check_postfix_bcc('sender_bcc_maps'): |
| bcc_conf += 'sender_bcc_maps = hash:/etc/postfix/sender_bcc\n' |
| if bcc_conf: |
| public.writeFile(self.postfix_main_cf, '\n'+bcc_conf, 'a+') |
|
|
| shell_str = ''' |
| postmap /etc/postfix/recipient_bcc |
| postmap /etc/postfix/sender_bcc |
| systemctl reload postfix |
| ''' |
| public.ExecShell(shell_str) |
| return self.return_msg(public.returnMsg(True, "设置成功")) |
|
|
| def check_postfix_bcc(self, act): |
| try: |
| res = public.ExecShell('postconf {}'.format(act)) |
| if '=' in res[0] and res[0].split('=')[1].strip(): |
| return True |
| else: |
| return False |
| except: |
| return False |
|
|
| |
| def del_bcc(self, args): |
| data = self.get_bcc(args)['data'] |
| bcc_file = "/etc/postfix/{}_bcc".format(args.type) |
| |
| conf = public.readFile(bcc_file) |
| n = 0 |
| rep = r"\n*{}\s+{}".format(args.user, args.forward_user) |
| for d in data[args.type]: |
| if args.user == d["user"] and args.forward_user == d["forward_user"]: |
| del (data[args.type][n]) |
| public.writeFile(self._forward_conf, json.dumps(data)) |
| conf = re.sub(rep, '', conf) |
| public.writeFile(bcc_file, conf) |
| public.ExecShell( |
| 'postmap {} && systemctl reload postfix'.format(bcc_file)) |
| return self.return_msg(public.returnMsg(True, '删除成功')) |
| n += 1 |
| return self.return_msg(public.returnMsg(True, '删除失败')) |
|
|
| |
| def update_bcc(self, args): |
| self.del_bcc(args) |
| args.type = args.type_new |
| args.forward_user = args.forward_user_new |
| args.active = args.active_new |
| self.set_mail_bcc(args) |
| return self.return_msg(public.returnMsg(True, '修改成功')) |
|
|
| |
| def set_smtp_relay(self, args): |
| """ |
| username: mailgun的用户名 |
| passwd: mailgun的密码 |
| smtphost: smtp地址 |
| port: smtp端口 |
| """ |
| username = args.username |
| passwd = args.passwd |
| smtphost = args.smtphost |
| port = args.port |
| add_paramater = """ |
| #BEGIN_POSTFIX_RELAY |
| relayhost = [{smtphost}]:{port} |
| smtp_sasl_auth_enable = yes |
| smtp_sasl_password_maps = static:{username}:{passwd} |
| smtp_sasl_security_options = noanonymous |
| #END_POSTFIX_RELAY |
| """.format(smtphost=smtphost, port=port, username=username, passwd=passwd) |
| if self.get_smtp_status(args)['status']: |
| return self.return_msg(public.returnMsg(False, "smtp中继配置已经存在")) |
| public.writeFile(self.postfix_main_cf, add_paramater, 'a+') |
| return self.return_msg(public.returnMsg(True, "设置邮件中继成功")) |
|
|
| |
| def get_smtp_status(self, args): |
| conf = public.readFile(self.postfix_main_cf) |
| if not conf: |
| return self.return_msg(public.returnMsg(False, "没有找到中继配置信息")) |
| if "BEGIN_POSTFIX_RELAY" in conf: |
| host_port_reg = r"relayhost\s*=\s*\[([\.\w]+)\]:(\d+)" |
| tmp = re.search(host_port_reg, conf) |
| host = port = user = passwd = "" |
| if tmp: |
| host = tmp.groups(1)[0] |
| port = tmp.groups(2)[1] |
| user_passwd_reg = r"smtp_sasl_password_maps\s*=\s*static:(.*?):(.*)" |
| tmp = re.search(user_passwd_reg, conf) |
| if tmp: |
| user = tmp.groups(1)[0] |
| passwd = tmp.groups(2)[1] |
| return self.return_msg(public.returnMsg(True, { |
| "host": host, |
| "port": port, |
| "user": user, |
| "passwd": passwd |
| })) |
| return self.return_msg(public.returnMsg(False, "没有找到中继配置信息")) |
|
|
| |
| def cancel_smtp_relay(self, args): |
| conf = public.readFile(self.postfix_main_cf) |
| reg = r"\n#BEGIN_POSTFIX_RELAY(.|\n)+#END_POSTFIX_RELAY\n" |
| tmp = re.search(reg, conf) |
| if not tmp: |
| return self.return_msg(public.returnMsg(False, "smtp中继配置不存在")) |
| conf = re.sub(reg, "", conf) |
| public.writeFile(self.postfix_main_cf, conf) |
| return self.return_msg(public.returnMsg(True, "取消邮件中继成功")) |
|
|
| |
| def _get_anti_server_ip_port(self, get): |
| conf = public.readFile('/etc/amavisd/amavisd.conf') |
| if not os.path.exists('/etc/redhat-release'): |
| conf = public.readFile('/etc/amavis/conf.d/20-debian_defaults') |
| reg = r'\n\${}\s*=\s*[\'\"]?(.*?)[\'\"]?;' |
| spam_server_ip_reg = reg.format('inet_socket_bind') |
| spam_server_port_reg = reg.format('inet_socket_port') |
| spam_server_ip = re.search(spam_server_ip_reg, conf) |
| if spam_server_ip: |
| spam_server_ip = spam_server_ip.groups(1)[0] |
| else: |
| spam_server_ip = '127.0.0.1' |
| spam_server_port = re.search(spam_server_port_reg, conf) |
| if spam_server_port: |
| spam_server_port = spam_server_port.groups(1)[0] |
| else: |
| spam_server_port = '10024' |
| return self.return_msg({ |
| 'spam_server_port': spam_server_port, |
| 'spam_server_ip': spam_server_ip |
| }) |
|
|
| |
| def _set_main_cf_anti_spam(self, args): |
| conf = public.readFile(self.postfix_main_cf) |
| anti_spam_conf = """ |
| ##BT-ANTISPAM-BEGIN |
| content_filter=amavisfeed:[{}]:{} |
| ##BT-ANTISPAM-END |
| """ |
| if 'amavisfeed' in conf: |
| return |
| if args.spam_server_ip == 'localhost': |
| spam_server_info = self._get_anti_server_ip_port(get=None) |
| anti_spam_conf = anti_spam_conf.format(spam_server_info['spam_server_ip'], |
| spam_server_info['spam_server_port']) |
| public.writeFile(self.postfix_main_cf, conf + anti_spam_conf) |
| else: |
| anti_spam_conf = anti_spam_conf.format(args.spam_server_ip, args.spam_server_port) |
| public.writeFile(self.postfix_main_cf, conf + anti_spam_conf) |
|
|
| |
| def _set_master_cf_anti_spam(self): |
| master_file = '/etc/postfix/master.cf' |
| conf = public.readFile(master_file) |
| if re.search('##BT-ANTISPAM-BEGIN', conf): |
| return |
| anti_conf = """ |
| ##BT-ANTISPAM-BEGIN |
| amavisfeed unix - - n - 2 smtp |
| -o smtp_data_done_timeout=1000 |
| -o smtp_send_xforward_command=yes |
| -o disable_dns_lookups=yes |
| -o max_use=20 |
| 127.0.0.1:10025 inet n - n - - smtpd |
| -o content_filter= |
| -o smtpd_delay_reject=no |
| -o smtpd_client_restrictions=permit_mynetworks,reject |
| -o smtpd_helo_restrictions= |
| -o smtpd_sender_restrictions= |
| -o smtpd_recipient_restrictions=permit_mynetworks,reject |
| -o smtpd_data_restrictions=reject_unauth_pipelining |
| -o smtpd_end_of_data_restrictions= |
| -o smtpd_restriction_classes= |
| -o mynetworks=127.0.0.0/8,192.168.0.0/16 |
| -o smtpd_error_sleep_time=0 |
| -o smtpd_soft_error_limit=1001 |
| -o smtpd_hard_error_limit=1000 |
| -o smtpd_client_connection_count_limit=0 |
| -o smtpd_client_connection_rate_limit=0 |
| -o receive_override_options=no_header_body_checks,no_unknown_recipient_checks,no_milters |
| -o local_header_rewrite_clients= |
| ##BT-ANTISPAM-END |
| """ |
| public.writeFile(master_file, conf + anti_conf) |
|
|
| def _set_dovecot_cf_anti_spam(self): |
| ''' |
| 设置dovecot配置支持反垃圾 |
| :return: |
| ''' |
| |
| if os.path.exists('/etc/dovecot/conf.d/90-sieve.conf'): |
| download_conf_shell = ''' |
| wget "{download_conf_url}/mail_sys/dovecot/dovecot.conf" -O /etc/dovecot/dovecot.conf -T 10 |
| wget "{download_conf_url}/mail_sys/dovecot/15-lda.conf" -O /etc/dovecot/conf.d/15-lda.conf -T 10 |
| wget "{download_conf_url}/mail_sys/dovecot/20-lmtp.conf" -O /etc/dovecot/conf.d/20-lmtp.conf -T 10 |
| wget "{download_conf_url}/mail_sys/dovecot/90-plugin.conf" -O /etc/dovecot/conf.d/90-plugin.conf -T 10 |
| wget "{download_conf_url}/mail_sys/dovecot/90-sieve.conf" -O /etc/dovecot/conf.d/90-sieve.conf -T 10 |
| '''.format(download_conf_url=public.get_url()) |
| public.ExecShell(download_conf_shell) |
| if not os.path.exists('/etc/dovecot/sieve'): |
| os.makedirs('/etc/dovecot/sieve') |
| default_sieve = '''require "fileinto"; |
| if header :contains "X-Spam-Flag" "YES" { |
| fileinto "Junk"; |
| }''' |
| public.writeFile('/etc/dovecot/sieve/default.sieve', default_sieve) |
| public.ExecShell('chown -R vmail:dovecot /etc/dovecot') |
|
|
| |
| def turn_on_anti_spam(self, args): |
| if args.spam_server_ip != 'localhost': |
| return self.return_msg(public.returnMsg(False, '目前还不支持远程扫描,正在测试该功能')) |
| if args.spam_server_ip == 'localhost' and not os.path.exists( |
| '/www/server/panel/plugin/anti_spam'): |
| return self.return_msg(public.returnMsg(False, '请先安装[反垃圾邮件网关]插件,并将要监听的域名添加到配置里面')) |
| self._set_main_cf_anti_spam(args) |
| self._set_master_cf_anti_spam() |
| self._set_dovecot_cf_anti_spam() |
| public.ExecShell('/usr/sbin/postfix reload') |
| public.ExecShell('systemctl restart dovecot') |
| public.ExecShell('systemctl restart spamassassin') |
| return self.return_msg(public.returnMsg(True, '开启成功')) |
|
|
| |
| def turn_off_anti_spam(self, args): |
| |
| master_file = '/etc/postfix/master.cf' |
| conf = public.readFile(master_file) |
| reg = "\n##BT-ANTISPAM-BEGIN(.|\n)+##BT-ANTISPAM-END\n" |
| conf = re.sub(reg, '', conf) |
| public.writeFile(master_file, conf) |
| |
| conf = public.readFile(self.postfix_main_cf) |
| conf = re.sub(reg, '', conf) |
| public.writeFile(self.postfix_main_cf, conf) |
| public.ExecShell('/usr/sbin/postfix reload') |
| return self.return_msg(public.returnMsg(True, '关闭成功')) |
|
|
| |
| def get_anti_spam_status(self, args): |
| conf = public.readFile(self.postfix_main_cf) |
| if re.search('##BT-ANTISPAM-BEGIN', conf): |
| return self.return_msg(True) |
| return self.return_msg(False) |
|
|
| |
| def get_backup_task_status(self, get): |
| c_id = public.M('crontab').where('name=?', |
| u'[勿删]堡塔邮局-数据备份任务').getField('id') |
| if not c_id: return self.return_msg(public.returnMsg(False, '定时任务不存在!')) |
| data = public.M('crontab').where('name=?', u'[勿删]堡塔邮局-数据备份任务').find() |
| return self.return_msg(public.returnMsg(True, data)) |
|
|
| |
| def open_backup_task(self, get): |
| import crontab |
| p = crontab.crontab() |
|
|
| c_id = public.M('crontab').where('name=?', |
| u'[勿删]堡塔邮局-数据备份任务').getField('id') |
| if c_id: |
| data = {} |
| data['id'] = c_id |
| data['name'] = u'[勿删]堡塔邮局-数据备份任务' |
| data['type'] = get.type |
| data['where1'] = get.where1 if 'where1' in get else '' |
| data['sBody'] = '' |
| data['backupTo'] = get.backupTo if 'backupTo' in get else 'localhost' |
| data['sType'] = 'path' |
| data['hour'] = get.hour if 'hour' in get else '' |
| data['minute'] = get.minute if 'minute' in get else '' |
| data['week'] = get.week if 'week' in get else '' |
| data['sName'] = '/www/vmail/' |
| data['urladdress'] = '' |
| data['save'] = get.save |
| p.modify_crond(data) |
| return self.return_msg(public.returnMsg(True, '编辑成功!')) |
| else: |
| data = {} |
| data['name'] = u'[勿删]堡塔邮局-数据备份任务' |
| data['type'] = get.type |
| data['where1'] = get.where1 if 'where1' in get else '' |
| data['sBody'] = '' |
| data['backupTo'] = get.backupTo if 'backupTo' in get else 'localhost' |
| data['sType'] = 'path' |
| data['hour'] = get.hour if 'hour' in get else '' |
| data['minute'] = get.minute if 'minute' in get else '' |
| data['week'] = get.week if 'week' in get else '' |
| data['sName'] = '/www/vmail/' |
| data['urladdress'] = '' |
| data['save'] = get.save |
| p.AddCrontab(data) |
| return self.return_msg(public.returnMsg(True, '设置成功!')) |
|
|
| |
| def close_backup_task(self, get): |
| import crontab |
|
|
| p = crontab.crontab() |
| c_id = public.M('crontab').where('name=?', |
| u'[勿删]堡塔邮局-数据备份任务').getField('id') |
| if not c_id: return self.return_msg(public.returnMsg(False, '定时任务不存在!')) |
| args = {"id": c_id} |
| p.DelCrontab(args) |
| return self.return_msg(public.returnMsg(True, '关闭成功!')) |
|
|
| |
| def get_cloud_storage_list(self, get): |
| data = [] |
| tmp = public.readFile('data/libList.conf') |
| libs = json.loads(tmp) |
| for lib in libs: |
| if 'opt' not in lib: continue |
| filename = 'plugin/{}'.format(lib['opt']) |
| if not os.path.exists(filename): continue |
| data.append({'name': lib['name'], 'value': lib['opt']}) |
| return self.return_msg(data) |
|
|
| |
| def get_backup_file_list(self, get): |
| dir_path = get.path.strip() |
| if not os.path.exists(dir_path): |
| os.makedirs(dir_path, 384) |
| dir_path_vmail = os.path.join(dir_path, 'vmail') |
| files = [] |
| for file_name in os.listdir(dir_path): |
| if not file_name.startswith('path_vmail'): continue |
| file_path = os.path.join(dir_path, file_name) |
| if not os.path.exists(file_path): continue |
| f_info = {} |
| f_info['name'] = file_name |
| f_info['mtime'] = os.path.getmtime(file_path) |
| files.append(f_info) |
| if os.path.exists(dir_path_vmail): |
| for file_name in os.listdir(dir_path_vmail): |
| if not file_name.startswith('path_vmail'): continue |
| file_path = os.path.join(dir_path_vmail, file_name) |
| if not os.path.exists(file_path): continue |
| f_info = {} |
| f_info['name'] = 'vmail/'+ file_name |
| f_info['mtime'] = os.path.getmtime(file_path) |
| files.append(f_info) |
| files = sorted(files, key=lambda x: x['mtime'], reverse=True) |
| return self.return_msg(files) |
|
|
| def get_backup_path(self, get): |
| path = public.M('config').where("id=?", (1,)).getField('backup_path') |
| path = os.path.join(path, 'path') |
| return self.return_msg(os.path.join(path, 'vmail')) |
|
|
| |
| def restore(self, get): |
| file_path = get.file_path.strip() |
| if not os.path.exists(file_path): |
| return self.return_msg(public.returnMsg(False, '文件不存在')) |
| |
| |
| file_name = os.path.basename(file_path) |
| if file_name.startswith('path_vmail') and file_name.endswith('.tar.gz'): |
| cmd = 'cd {} && tar -xzvf {} 2>&1'.format('/www', file_path) |
| print(cmd) |
| public.ExecShell(cmd) |
| return self.return_msg(public.returnMsg(True, '恢复数据完成')) |
| else: |
| return self.return_msg(public.returnMsg(False, '这不是有效的备份文件!,文件名应该以"path_vmail"开头,以".tar.gz"结尾')) |
|
|
| |
| def set_save_day(self, get): |
| |
| |
| skey = "mail_save_day" |
| cache.set(skey, get.save_day, 86400) |
|
|
| public.writeFile(self._save_conf, get.save_day) |
| return self.return_msg(public.returnMsg(True, '设置成功')) |
|
|
| |
| def get_save_day(self, get): |
| |
| skey = "mail_save_day" |
| cache_day = cache.get(skey) |
| if cache_day: |
| return self.return_msg(int(cache_day)) |
| if not os.path.exists(self._save_conf): |
| return self.return_msg(0) |
| save_day = int(public.readFile(self._save_conf)) |
| cache.set(skey, save_day, 86400) |
| return self.return_msg(save_day) |
|
|
| def _get_old_certificate_path(self, conf): |
| |
| cert_file_reg = r'#smtpd_tls_cert_file\s*=\s*(.*)' |
| cert_key_reg = r'#smtpd_tls_key_file\s*=\s*(.*)' |
| cert_tmp = re.search(cert_file_reg, conf) |
| if cert_tmp: |
| cert_file = cert_tmp.groups(1)[0] |
| cert_key = re.search(cert_key_reg, conf).groups(1)[0] |
| else: |
| cert_key = '/etc/pki/dovecot/private/dovecot.pem' |
| cert_file = '/etc/pki/dovecot/certs/dovecot.pem' |
| return {'cert_key': cert_key, 'cert_file': cert_file} |
|
|
| def _set_new_certificate_conf(self, conf, cert_file, cert_key): |
| """添加新的证书配置,支持多域名不同证书""" |
| |
| sni_reg = r'\ntls_server_sni_maps\s*=(.*)' |
| if not re.search(sni_reg, conf): |
| conf += '\ntls_server_sni_maps = hash:/etc/postfix/vmail_ssl.map\n' |
|
|
| |
| |
| cert_reg = r'\nsmtpd_tls_cert_file\s*=(.*)' |
| key_reg = r'\nsmtpd_tls_key_file\s*=(.*)' |
|
|
| if re.search(cert_reg, conf): |
| conf = re.sub(cert_reg, '\nsmtpd_tls_cert_file = {}'.format(cert_file), conf) |
| else: |
| conf += '\nsmtpd_tls_cert_file = {}'.format(cert_file) |
|
|
| if re.search(key_reg, conf): |
| conf = re.sub(key_reg, '\nsmtpd_tls_key_file = {}'.format(cert_key), conf) |
| else: |
| conf += '\nsmtpd_tls_key_file = {}'.format(cert_key) |
|
|
| |
| chain_reg = r'\nsmtpd_tls_chain_files\s*=(.*)' |
| if re.search(chain_reg, conf): |
| conf = re.sub(chain_reg, '', conf) |
|
|
| return conf |
|
|
| def _set_vmail_certificate(self, args, arecord, cert_file, cert_key): |
| """设置证书给某个A记录和域名,完善SNI映射""" |
| domain = args.domain |
| if args.act == 'add': |
| vmail_ssl_map = '/etc/postfix/vmail_ssl.map' |
| |
| map_content = "" |
| if os.path.isfile(vmail_ssl_map): |
| map_content = public.readFile(vmail_ssl_map) |
| if map_content is None: |
| map_content = "" |
|
|
| |
| domain_map_line = '{} {} {}\n'.format(domain, cert_key, cert_file) |
| arecord_map_line = '{} {} {}\n'.format(arecord, cert_key, cert_file) |
|
|
| |
| if re.search(r'^{}.*$'.format(domain), map_content, re.M): |
| map_content = re.sub(r'^{}.*$'.format(domain), domain_map_line.strip(), map_content, flags=re.M) |
| else: |
| map_content += domain_map_line |
|
|
| |
| if re.search(r'^{}.*$'.format(arecord), map_content, re.M): |
| map_content = re.sub(r'^{}.*$'.format(arecord), arecord_map_line.strip(), map_content, flags=re.M) |
| else: |
| map_content += arecord_map_line |
|
|
| |
| public.writeFile(vmail_ssl_map, map_content) |
| os.system('postmap -F hash:{}'.format(vmail_ssl_map)) |
| else: |
| |
| vmail_ssl_map = '/etc/postfix/vmail_ssl.map' |
| if os.path.exists(vmail_ssl_map): |
| map_content = public.readFile(vmail_ssl_map) |
| if map_content: |
| |
| map_content = re.sub(r'^{}.*$\n?'.format(domain), '', map_content, flags=re.M) |
| map_content = re.sub(r'^{}.*$\n?'.format(arecord), '', map_content, flags=re.M) |
| public.writeFile(vmail_ssl_map, map_content) |
| os.system('postmap -F hash:{}'.format(vmail_ssl_map)) |
|
|
| def _set_dovecot_cert_global(self, cert_file, cert_key, conf): |
| default_cert_key = r'ssl_key\s*=\s*<\s*/etc/pki/dovecot/private/dovecot.pem' |
| default_cert_file = r'ssl_cert\s*=\s*<\s*/etc/pki/dovecot/certs/dovecot.pem' |
| if not re.search(default_cert_file, conf): |
| return conf |
| conf = re.sub(default_cert_file, "ssl_cert = <{}".format(cert_file), conf) |
| conf = re.sub(default_cert_key, "ssl_key = <{}".format(cert_key), conf) |
| return conf |
|
|
| |
| def _set_dovecot_certificate(self, args, a_record, cert_file, cert_key): |
| dovecot_version = self.get_dovecot_version()['data'] |
| ssl_file = "/etc/dovecot/conf.d/10-ssl.conf" |
| ssl_conf = public.readFile(ssl_file) |
| if not ssl_conf: |
| return public.returnMsg(False, |
| "找不到dovecot配置文件 {}".format(ssl_file)) |
| |
| if dovecot_version.startswith('2.3'): |
| if args.act == 'add': |
| if not os.path.exists('/etc/dovecot/dh.pem') or os.path.getsize('/etc/dovecot/dh.pem') < 300: |
| public.ExecShell('openssl dhparam 2048 > /etc/dovecot/dh.pem') |
| if 'ssl_dh = </etc/dovecot/dh.pem' not in ssl_conf: |
| ssl_conf = ssl_conf + "\nssl_dh = </etc/dovecot/dh.pem" |
| |
| reg_cert = r'local_name\s+{}'.format(a_record) |
| if args.act == 'add' and not re.search(reg_cert, ssl_conf): |
| ssl_conf = self._set_dovecot_cert_global(cert_file, cert_key, ssl_conf) |
| domain_ssl_conf = """ |
| #DOMAIN_SSL_BEGIN_%s |
| local_name %s { |
| ssl_cert = < %s |
| ssl_key = < %s |
| } |
| #DOMAIN_SSL_END_%s""" % (a_record, a_record, cert_file, cert_key, a_record) |
| reg = r'ssl\s*=\s*yes' |
| ssl_conf = re.sub(reg, 'ssl = yes' + domain_ssl_conf, ssl_conf) |
| if args.act == 'delete': |
| reg = '#DOMAIN_SSL_BEGIN_{a}(.|\n)+#DOMAIN_SSL_END_{a}\n'.format(a=a_record) |
| ssl_conf = re.sub(reg, '', ssl_conf) |
|
|
| public.writeFile(ssl_file, ssl_conf) |
| public.ExecShell('systemctl restart dovecot') |
|
|
| def _verify_certificate(self, args, path, csrpath, keypath): |
| |
| |
| |
| |
| backup_cert = '/tmp/backup_cert_mail_sys' |
| if hasattr(args, "act") and args.act == "add": |
| if args.key.find('KEY') == -1: |
| return public.returnMsg(False, '私钥错误,请检查!') |
| if args.csr.find('CERTIFICATE') == -1: |
| return public.returnMsg(False, '证书错误,请检查!') |
| public.writeFile('/tmp/mail_cert.pl', str(args.csr)) |
| if not public.CheckCert('/tmp/mail_cert.pl'): |
| return public.returnMsg(False, '证书错误,请以pem格式粘贴正确的证书!') |
| if os.path.exists(backup_cert): shutil.rmtree(backup_cert) |
| if os.path.exists(path): shutil.move(path, backup_cert) |
| if os.path.exists(path): shutil.rmtree(path) |
| os.makedirs(path) |
| public.writeFile(keypath, args.key) |
| os.chown(keypath, 0, 0) |
| os.chmod(keypath, 0o600) |
| public.writeFile(csrpath, args.csr) |
| os.chown(csrpath, 0, 0) |
| os.chmod(csrpath, 0o600) |
| |
| |
| |
|
|
| def _check_postfix_conf(self): |
| result = public.process_exists('master', '/usr/libexec/postfix/master') |
| if "ubuntu" in self.sys_v: |
| result = public.process_exists('master', '/usr/lib/postfix/sbin/master') |
| return result |
|
|
| def _get_ubuntu_version(self): |
| return public.readFile('/etc/issue').strip().split("\n")[0].replace('\\n', '').replace(r'\l', |
| '').strip().lower() |
|
|
| def _modify_old_ssl_perameter(self, conf): |
| if not os.path.exists('/etc/postfix/vmail_ssl.map'): |
| |
| if '#smtpd_tls_cert_file' not in conf: |
| conf = conf.replace('smtpd_tls_cert_file', '#smtpd_tls_cert_file') |
| conf = conf.replace('smtpd_tls_key_file', '#smtpd_tls_key_file') |
| |
| old_cert_info = self._get_old_certificate_path(conf) |
| |
| if 'tls_server_sni_maps' not in conf: |
| conf = self._set_new_certificate_conf(conf, old_cert_info['cert_file'], old_cert_info['cert_key']) |
| public.writeFile(self.postfix_main_cf, conf) |
|
|
| def _fix_default_cert(self, conf, cert_file, cert_key): |
| reg = r'smtpd_tls_chain_files\s*=(.*)' |
| tmp = re.search(reg, conf) |
| if not tmp: |
| return conf |
| tmp = tmp.groups()[0] |
| if len(tmp) < 5 or 'dovecot.pem' in conf: |
| conf = self._set_new_certificate_conf(conf, cert_file, cert_key) |
| return conf |
|
|
| def _set_master_ssl(self): |
| master_file = "/etc/postfix/master.cf" |
| master_conf = public.readFile(master_file) |
| master_rep = r"\n*#\s*-o\s+smtpd_tls_auth_only=yes" |
| master_str = "\n -o smtpd_tls_auth_only=yes" |
| master_rep1 = r"\n*#\s*-o\s+smtpd_tls_wrappermode=yes" |
| master_str1 = "\n -o smtpd_tls_wrappermode=yes" |
| master_conf = re.sub(master_rep, master_str, master_conf) |
| master_conf = re.sub(master_rep1, master_str1, master_conf) |
| public.writeFile(master_file, master_conf) |
|
|
| def set_mail_certificate_multiple(self, args): |
| ''' |
| :param args: domain 要设置证书的域名 |
| :param args: csr |
| :param args: key |
| :param args: act add/delete |
| :return: |
| ''' |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| conf = public.readFile(self.postfix_main_cf) |
| domain = args.domain |
| cert_path = '/www/server/panel/plugin/mail_sys/cert/{}'.format(domain) |
| cert_file = "{}/fullchain.pem".format(cert_path) |
| cert_key = "{}/privkey.pem".format(cert_path) |
| if not os.path.exists(cert_path): |
| os.makedirs(cert_path) |
| |
| self.back_file(self.postfix_main_cf) |
| |
| |
| conf = self._fix_default_cert(conf, cert_file, cert_key) |
| self._modify_old_ssl_perameter(conf) |
| |
| self._set_master_ssl() |
| |
| arecord = self.M('domain').where('domain=?', domain).field('a_record').find()['a_record'] |
| if arecord == '': |
| return self.return_msg(public.returnMsg(False, '所设置的域名不存在')) |
| |
| if args.csr != '': |
| verify_result = self._verify_certificate(args, cert_path, cert_file, cert_key) |
| if verify_result: |
| return self.return_msg(verify_result) |
| |
| self._set_vmail_certificate(args, arecord, cert_file, cert_key) |
| self._set_dovecot_certificate(args, arecord, cert_file, cert_key) |
| |
| |
| |
| |
| |
| |
| |
| public.ExecShell('postmap -F hash:/etc/postfix/vmail_ssl.map && systemctl restart postfix') |
| if not self._check_postfix_conf(): |
| self.restore_file(self.postfix_main_cf) |
| return self.return_msg(public.returnMsg(False, '设置失败,恢复配置文件')) |
| return self.return_msg(public.returnMsg(True, '设置成功')) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| def _get_multiple_certificate_domain_status(self, domain): |
| path = '/www/server/panel/plugin/mail_sys/cert/{}/fullchain.pem'.format(domain) |
| ssl_conf = public.readFile('/etc/postfix/vmail_ssl.map') |
| |
| |
| |
| if not os.path.exists(path): |
| return False |
| if not ssl_conf or domain not in ssl_conf: |
| return False |
| return True |
|
|
| |
| def back_file(self, file, act=None): |
| """ |
| @name 备份配置文件 |
| @author zhwen<zhw@bt.cn> |
| @param file 需要备份的文件 |
| @param act 如果存在,则备份一份作为默认配置 |
| """ |
| file_type = "_bak" |
| if act: |
| file_type = "_def" |
| public.ExecShell("/usr/bin/cp -p {0} {1}".format( |
| file, file + file_type)) |
|
|
| |
| def restore_file(self, file, act=None): |
| """ |
| @name 还原配置文件 |
| @author zhwen<zhw@bt.cn> |
| @param file 需要还原的文件 |
| @param act 如果存在,则还原默认配置 |
| """ |
| file_type = "_bak" |
| if act: |
| file_type = "_def" |
| public.ExecShell("/usr/bin/cp -p {1} {0}".format( |
| file, file + file_type)) |
|
|
| def enable_catchall(self, args): |
| """ |
| 设置邮局捕获所有/不存在的用户并转发到指定邮箱 |
| @param args.domain: 需要捕获的域名 |
| @param args.email: 转发到的邮箱 |
| @param args.catch_type: 捕获类型 all/none 全部/不存在的用户 默认none |
| @return: |
| """ |
| if not self.check_main_forward_conf(): |
| return self.return_msg(public.returnMsg(False, 'main.cf配置失败')) |
|
|
| domain = '@' + args.domain.strip() |
| email = args.email.strip() |
|
|
| self._deledte_catchall(args.domain) |
| create_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) |
|
|
| if email: |
| if args.catch_type == 'all': |
| domain = '%' + domain |
| self.M('alias').add('address,goto,domain,created,modified,active', |
| (domain, email, args.domain.strip(), create_time, create_time, '1')) |
| return self.return_msg(public.returnMsg(True, '设置成功')) |
| def _add_enable_catchall(self, args): |
| """ |
| 设置邮局捕获所有/不存在的用户并转发到指定邮箱 |
| @param args.domain: 需要捕获的域名 |
| @param args.email: 转发到的邮箱 |
| @param args.catch_type: 捕获类型 all/none 全部/不存在的用户 默认none |
| @return: |
| """ |
| domain = '@' + args.domain.strip() |
| email = args.email.strip() |
| catch_type = args.catch_type |
| if catch_type == 'all': |
| domain = '%' + domain |
|
|
| create_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) |
| self.M('alias').add('address,goto,domain,created,modified,active', |
| (domain, email, args.domain.strip(), create_time, create_time, '1')) |
| self.check_main_forward_conf() |
| return True |
|
|
| def check_main_forward_conf(self): |
| """ |
| 检查main.cf配置文件中的virtual_alias_maps配置 |
| @return: |
| """ |
| conf = public.readFile(self.postfix_main_cf) |
| if not conf: |
| return False |
| virtual_alias_maps = 'virtual_alias_maps = sqlite:/etc/postfix/sqlite_virtual_alias_maps.cf, sqlite:/etc/postfix/sqlite_virtual_alias_domain_maps.cf, sqlite:/etc/postfix/sqlite_virtual_alias_domain_catchall_maps.cf,sqlite:/etc/postfix/bt_catchnone_maps.cf' |
| if virtual_alias_maps in conf: |
| return True |
| else: |
| try: |
| if 'virtual_alias_maps' in conf: |
| conf = re.sub(r'virtual_alias_maps\s*=.*', virtual_alias_maps, conf) |
| else: |
| conf += '\n'+virtual_alias_maps+'\n' |
| public.writeFile(self.postfix_main_cf, conf) |
| public.ExecShell('systemctl restart postfix') |
| return True |
| except: |
| public.print_log(public.get_error_info()) |
| return False |
|
|
| def _get_catchall_status(self, domain): |
| """ |
| 获取某个域名下的catchall开启状态和类型 |
| @param domain: |
| @return: |
| """ |
| conf = public.readFile(self.postfix_main_cf) |
| if not conf: |
| return False, '', '' |
| domain = '@' + domain.strip() |
| result = self.M('alias').where('(address=? or address=?) and active=1', (domain, "%"+domain)).select() |
| if result: |
| goto = '' |
| for i in result: |
| goto = i['goto'] |
| if '%' in i['address']: |
| return True, 'all', goto |
| return True, 'none', goto |
| return False, '', '' |
|
|
| def get_junk_mails(self, args): |
| ''' |
| 获取垃圾邮件列表 |
| :param args: |
| :return: |
| ''' |
| import email |
| from mailModel import receive_mail |
| reload(receive_mail) |
|
|
| if 'username' not in args: |
| return self.return_msg(public.returnMsg(False, '请传入账号名')) |
| username = args.username |
| if '@' not in username: |
| return self.return_msg(public.returnMsg(False, '账号名不合法')) |
| local_part, domain = username.split('@') |
| if 'p' not in args: |
| args.p = 1 |
| if 'p=' in args.p: |
| args.p = args.p.replace('p=', '') |
|
|
| receive_mail_client = receive_mail.ReceiveMail() |
| mail_list = [] |
| try: |
| dir_path = '/www/vmail/{0}/{1}/.Junk/cur'.format(domain, local_part) |
| if os.path.isdir(dir_path): |
| |
| new_path = '/www/vmail/{0}/{1}/.Junk/new'.format(domain, local_part) |
| if os.path.isdir(new_path): |
| for file in os.listdir(new_path): |
| src = os.path.join(new_path, file) |
| dst = os.path.join(dir_path, file) |
| shutil.move(src, dst) |
| files = [] |
| for fname in os.listdir(dir_path): |
| mail_file = os.path.join(dir_path, fname) |
| if not os.path.exists(mail_file): continue |
| f_info = {} |
| f_info['name'] = fname |
| f_info['mtime'] = os.path.getmtime(mail_file) |
| save_day = self.get_save_day(None)['data'] |
| if save_day > 0: |
| deltime = int(time.time()) - save_day * 86400 |
| if int(f_info['mtime']) < deltime: |
| os.remove(mail_file) |
| continue |
| files.append(f_info) |
| files = sorted(files, key=lambda x: x['mtime'], reverse=True) |
| page_data = public.get_page(len(files), int(args.p), 10) |
| |
| |
| pattern = r"href='(?:/v2)?/plugin.*?\?p=(\d+)'" |
| |
| page_data['page'] = re.sub(pattern, r"href='\1'", page_data['page']) |
| shift = int(page_data['shift']) |
| row = int(page_data['row']) |
| files = files[shift:shift + row] |
| for d in files: |
| mail_file = os.path.join(dir_path, d['name']) |
| try: |
| mailInfo = receive_mail_client.getMailInfo(public.readFile(mail_file)) |
| mailInfo['path'] = mail_file |
| mail_list.append(mailInfo) |
| except: |
| public.print_log(public.get_error_info()) |
| continue |
| return self.return_msg({ |
| 'status': |
| True, |
| 'data': |
| mail_list, |
| 'page': |
| page_data['page'] |
| }) |
| else: |
| page_data = public.get_page(0, int(args.p), 10) |
| return self.return_msg({ |
| 'status': |
| True, |
| 'data': |
| mail_list, |
| 'page': |
| page_data['page'] |
| }) |
| except Exception as e: |
| print(public.get_error_info()) |
| return self.return_msg(public.returnMsg(False, '获取失败,错误原因[{0}]'.format(str(e)))) |
|
|
| def move_to_junk(self, get): |
| ''' |
| 将收件箱的邮件标记为垃圾邮件 |
| :param get: |
| :return: |
| ''' |
| if 'username' not in get: |
| return self.return_msg(public.returnMsg(False, '请传入账号名')) |
| username = get.username |
| if '@' not in username: |
| return self.return_msg(public.returnMsg(False, '账号名不合法')) |
| local_part, domain = username.split('@') |
|
|
| src = get.path.strip() |
|
|
| if not os.path.exists('/www/vmail/{0}/{1}/.Junk'.format(domain, local_part)): |
| data = self.M('mailbox').where('username=?', username).field('password_encode,full_name').find() |
| password = self._decode(data['password_encode']) |
| self.create_mail_box(username, password) |
| if not os.path.exists(src): |
| return self.return_msg(public.returnMsg(False, '邮件路径不存在')) |
| dir_path = '/www/vmail/{0}/{1}/.Junk/cur'.format(domain, local_part) |
| dst = os.path.join(dir_path, os.path.basename(src)) |
| shutil.move(src, dst) |
| return self.return_msg(public.returnMsg(True, '标记成功')) |
|
|
| def move_out_junk(self, get): |
| ''' |
| 将垃圾箱的邮件移动到收件箱 |
| :param get: |
| :return: |
| ''' |
| if 'username' not in get: |
| return self.return_msg(public.returnMsg(False, '请传入账号名')) |
| username = get.username |
| if '@' not in username: |
| return self.return_msg(public.returnMsg(False, '账号名不合法')) |
| local_part, domain = username.split('@') |
|
|
| src = get.path.strip() |
| if not os.path.exists(src): |
| return self.return_msg(public.returnMsg(False, '邮件路径不存在')) |
| dir_path = '/www/vmail/{0}/{1}/cur'.format(domain, local_part) |
| dst = os.path.join(dir_path, os.path.basename(src)) |
| shutil.move(src, dst) |
| return self.return_msg(public.returnMsg(True, '操作成功')) |
|
|
| |
| def get_ssl_info(self, domain): |
|
|
| try: |
| import data |
| fullchain_file = '/www/server/panel/plugin/mail_sys/cert/{}/fullchain.pem'.format(domain) |
| privkey_file = '/www/server/panel/plugin/mail_sys/cert/{}/privkey.pem'.format(domain) |
| if not os.path.exists(fullchain_file) or not os.path.exists(privkey_file): |
| return {'dns': [domain]} |
| os.chown(fullchain_file, 0, 0) |
| os.chmod(fullchain_file, 0o600) |
| os.chown(privkey_file, 0, 0) |
| os.chmod(privkey_file, 0o600) |
|
|
| ssl_info = data.data().get_cert_end(fullchain_file) |
| if not ssl_info: |
| return {'dns': [domain]} |
| ssl_info['src'] = public.readFile(fullchain_file) |
| ssl_info['key'] = public.readFile(privkey_file) |
| ssl_info['endtime'] = int( |
| int(time.mktime(time.strptime(ssl_info['notAfter'], "%Y-%m-%d")) - time.time()) / 86400) |
| return ssl_info |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return {'dns': [domain]} |
|
|
| |
| |
| def apply_cert(self, args): |
| """ |
| domains 邮箱域名 ['example.com'] |
| auth_to CloudFlareDns|email|token 当auth_to 为 dns时是需要手动添加解析 |
| auto_wildcard = 1 |
| auth_type = dns |
| :param args: |
| :return: |
| """ |
| import acme_v2 |
| domains = json.loads(args.domains) |
| apply_cert_module = acme_v2.acme_v2() |
| apply_cert = apply_cert_module.apply_cert(domains, 'dns', args.auth_to, auto_wildcard=1) |
| return self.return_msg(apply_cert) |
|
|
| |
| def apply_cert_manual(self, args): |
| """ |
| index |
| :param args: |
| :return: |
| """ |
| import acme_v2 |
| apply_cert_module = acme_v2.acme_v2() |
| return self.return_msg(apply_cert_module.apply_cert([], 'dns', 'dns', index=args.index)) |
|
|
| def check_rspamd_route(self, args): |
| panel_init = public.readFile("/www/server/panel/BTPanel/__init__.py") |
| if "proxy_rspamd_requests" in panel_init: |
| return self.return_msg(public.returnMsg(True, "")) |
| return self.return_msg(public.returnMsg(False, "")) |
|
|
| @staticmethod |
| def change_hostname(args): |
| hostname = args.hostname |
| rep_domain = r"^(?=^.{3,255}$)[a-zA-Z0-9\_\-][a-zA-Z0-9\_\-]{0,62}(\.[a-zA-Z0-9\_\-][a-zA-Z0-9\_\-]{0,62})+$" |
| if not re.search(rep_domain, hostname): |
| return self.return_msg(public.returnMsg(False, "请输入完整域名,例如 mail.bt.com),")) |
| public.ExecShell('hostnamectl set-hostname --static {}'.format(hostname)) |
| h = socket.gethostname() |
| if h == hostname: |
| return self.return_msg(public.returnMsg(True, "设置成功!")) |
| return self.return_msg(public.returnMsg(False, "设置失败!")) |
|
|
| def check_init_result(self, args): |
| """ |
| 检查安装结果: |
| 服务状态 |
| 配置文件完整性 |
| :return: |
| """ |
| result = dict() |
| result['missing_file'] = self.check_confile_completeness() |
| result['service_status'] = self.get_service_status() |
| return self.return_msg(result) |
|
|
| def check_confile_completeness(self): |
| file_list = public.readFile("{}/services_file.txt".format(self.__setupPath)) |
| if not file_list: |
| return ["%s/services_file.txt|{download_conf_url}/mail_sys" % self.__setupPath] |
| file_list = [i for i in file_list.split()] |
| missing_files = [] |
| for file in file_list: |
| tmp = public.readFile(file.split('|')[0]) |
| if not tmp: |
| missing_files.append(file) |
| return missing_files |
|
|
| @staticmethod |
| def get_init_log(args=None): |
| """ |
| 获取初始化日志 |
| :param args: |
| :return: |
| """ |
| logfile = '/tmp/mail_init.log' |
| return self.return_msg(public.returnMsg(True, public.GetNumLines(logfile, 50))) |
|
|
| @staticmethod |
| def check_smtp_port(args): |
| """ |
| 检查服务器能否连接其他服务的25端口 |
| :param args: |
| :return: |
| """ |
| domain = args.domain |
| rep_domain = r"^(?=^.{3,255}$)[a-zA-Z0-9\_\-][a-zA-Z0-9\_\-]{0,62}(\.[a-zA-Z0-9\_\-][a-zA-Z0-9\_\-]{0,62})+$" |
| if not re.search(rep_domain, domain): |
| return self.return_msg(public.returnMsg(False, "请输入完整域名,例如 smtp.qq.com)")) |
| sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| result = sock.connect_ex((domain, 25)) |
| if result == 0: |
| return self.return_msg(public.returnMsg(True, "25端口通信正常")) |
| return self.return_msg(public.returnMsg(False, "25端口通信失败,请联系你的主机提供商进行放行")) |
|
|
| def download_file(self, args): |
| filename = args.filename |
| tmp = filename.split('|') |
| local_file = tmp[0] |
| remote_file = tmp[1].format(download_conf_url="http://node.aapanel.com") |
| data = public.readFile("/www/server/panel/plugin/mail_sys/services_file.txt") |
| if not data: |
| return self.return_msg(public.returnMsg(False, "Get source file error!")) |
| if remote_file not in data or local_file not in data: |
| return self.return_msg(public.returnMsg(False, "There is no such file!")) |
| public.ExecShell( |
| "wget {remote_file} -O {local_file} -T 10 --no-check-certificate".format(remote_file=remote_file, |
| local_file=local_file)) |
| return self.return_msg(public.returnMsg(True, "重新下载成功!")) |
|
|
| |
|
|
| |
| def check_domains_blacklist(self): |
| ''' |
| 获取域名是否被列入垃圾域名 |
| :param |
| :return: dict |
| domains_info = { |
| 'kern123.top':{ |
| "is_blacklist": False, # 无黑名单记录 |
| "blacklist": [], |
| }, |
| 'moyumao.top': { |
| "is_blacklist": True, # 有黑名单记录 |
| "blacklist": ['dnsbl.sorbs.net'], |
| }, |
| } |
| ''' |
|
|
| data_list = self.M('domain').order('created desc').field('domain').select() |
| domain_list = [item['domain'] for item in data_list] |
| domains_info = {} |
| cache_key_template = "{}_checkBlacklist" |
|
|
| for domain in domain_list: |
| cache_key = cache_key_template.format(domain) |
| cached_result = cache.get(cache_key) |
| if cached_result is None: |
| cached_result = {"is_blacklist": False, "blacklist": []} |
| blacklist_info = self._check_spam_blacklist(domain) |
| cached_result.update(blacklist_info) |
| cache.set(cache_key, cached_result, 86400) |
| domains_info[domain] = cached_result |
|
|
| return domains_info |
|
|
| def check_domain_blacklist(self, domain): |
| ''' |
| 获取域名是否被列入垃圾域名 |
| :param |
| :return: dict |
| domain_info = { |
| "is_blacklist": False, # 无黑名单记录 |
| "blacklist": [], |
| } |
| ''' |
|
|
| cache_key = "{}_checkBlacklist".format(domain) |
| cached_result = cache.get(cache_key) |
| |
| if cached_result is None: |
| cached_result = self._check_spam_blacklist(domain) |
| cache.set(cache_key, cached_result, 86400) |
| return cached_result |
|
|
| |
| def _check_spam_blacklist(self, domain): |
| |
| blacklist_services = [ |
| "zen.spamhaus.org", |
| "bl.spamcop.net", |
| "dnsbl.sorbs.net", |
| "multi.surbl.org", |
| "bl.spamcop.net", |
| "http.dnsbl.sorbs.net", |
| "misc.dnsbl.sorbs.net", |
| "socks.dnsbl.sorbs.net", |
| "web.dnsbl.sorbs.net", |
| "rbl.spamlab.com", |
| "cbl.anti - spam.org.cn", |
| "httpbl.abuse.ch", |
| "virbl.bit.nl", |
| "dsn.rfc - ignorant.org", |
| "opm.tornevall.org", |
| "multi.surbl.org", |
| "relays.mail - abuse.org", |
| "rbl - plus.mail - abuse.org", |
| "rbl.interserver.net", |
| "dul.dnsbl.sorbs.net", |
| "smtp.dnsbl.sorbs.net", |
| "spam.dnsbl.sorbs.net", |
| "zombie.dnsbl.sorbs.net", |
| "drone.abuse.ch", |
| "rbl.suresupport.com", |
| "spamguard.leadmon.net", |
| "netblock.pedantic.org", |
| "blackholes.mail - abuse.org", |
| "dnsbl.dronebl.org", |
| "query.senderbase.org", |
| "csi.cloudmark.com", |
| "0spam - killlist.fusionzero.com", |
| "0spam.fusionzero.com", |
| "access.redhawk.org", |
| "all.rbl.jp", |
| "all.spam - rbl.fr", |
| "all.spamrats.com", |
| "aspews.ext.sorbs.net", |
| "b.barracudacentral.org", |
| "backscatter.spameatingmonkey.net", |
| "badnets.spameatingmonkey.net", |
| "bb.barracudacentral.org", |
| "bl.drmx.org", |
| "bl.konstant.no", |
| "bl.nszones.com", |
| "bl.spamcannibal.org", |
| "bl.spameatingmonkey.net", |
| "bl.spamstinks.com", |
| "black.junkemailfilter.com", |
| "blackholes.five - ten - sg.com", |
| "blacklist.sci.kun.nl", |
| "blacklist.woody.ch", |
| "bogons.cymru.com", |
| "bsb.empty.us", |
| "bsb.spamlookup.net", |
| "cart00ney.surriel.com", |
| "cbl.abuseat.org", |
| "cbl.anti - spam.org.cn", |
| "cblless.anti - spam.org.cn", |
| "cblplus.anti - spam.org.cn", |
| "cdl.anti - spam.org.cn", |
| "cidr.bl.mcafee.com", |
| "combined.rbl.msrbl.net", |
| "db.wpbl.info", |
| "dev.null.dk", |
| "dialups.visi.com", |
| "dnsbl - 0.uceprotect.net", |
| "dnsbl - 1.uceprotect.net", |
| "dnsbl - 2.uceprotect.net", |
| "dnsbl - 3.uceprotect.net", |
| "dnsbl.anticaptcha.net", |
| "dnsbl.aspnet.hu", |
| "dnsbl.inps.de", |
| "dnsbl.justspam.org", |
| "dnsbl.kempt.net", |
| "dnsbl.madavi.de", |
| "dnsbl.rizon.net", |
| "dnsbl.rv - soft.info", |
| "dnsbl.rymsho.ru", |
| "dnsbl.sorbs.net", |
| "dnsbl.zapbl.net", |
| "dnsrbl.swinog.ch", |
| "dul.pacifier.net", |
| "dyn.nszones.com", |
| "dyna.spamrats.com", |
| "fnrbl.fast.net", |
| "fresh.spameatingmonkey.net", |
| "hostkarma.junkemailfilter.com", |
| "images.rbl.msrbl.net", |
| "ips.backscatterer.org", |
| "ix.dnsbl.manitu.net", |
| "korea.services.net", |
| "l2.bbfh.ext.sorbs.net", |
| "l3.bbfh.ext.sorbs.net", |
| "l4.bbfh.ext.sorbs.net", |
| "list.bbfh.org", |
| "list.blogspambl.com", |
| "mail - abuse.blacklist.jippg.org", |
| "netbl.spameatingmonkey.net", |
| "netscan.rbl.blockedservers.com", |
| "no - more - funn.moensted.dk", |
| "noptr.spamrats.com", |
| "orvedb.aupads.org", |
| "pbl.spamhaus.org", |
| "phishing.rbl.msrbl.net", |
| "pofon.foobar.hu", |
| "psbl.surriel.com", |
| "rbl.abuse.ro", |
| "rbl.blockedservers.com", |
| "rbl.dns - servicios.com", |
| "rbl.efnet.org", |
| "rbl.efnetrbl.org", |
| "rbl.iprange.net", |
| "rbl.schulte.org", |
| "rbl.talkactive.net", |
| "rbl2.triumf.ca", |
| "rsbl.aupads.org", |
| "sbl - xbl.spamhaus.org", |
| "sbl.nszones.com", |
| "sbl.spamhaus.org", |
| "short.rbl.jp", |
| "spam.dnsbl.anonmails.de", |
| "spam.pedantic.org", |
| "spam.rbl.blockedservers.com", |
| "spam.rbl.msrbl.net", |
| "spam.spamrats.com", |
| "spamrbl.imp.ch", |
| "spamsources.fabel.dk", |
| "st.technovision.dk", |
| "tor.dan.me.uk", |
| "tor.dnsbl.sectoor.de", |
| "tor.efnet.org", |
| "torexit.dan.me.uk", |
| "truncate.gbudb.net", |
| "ubl.unsubscore.com", |
| "uribl.spameatingmonkey.net", |
| "urired.spameatingmonkey.net", |
| "virbl.dnsbl.bit.nl", |
| "virus.rbl.jp", |
| "virus.rbl.msrbl.net", |
| "vote.drbl.caravan.ru", |
| "vote.drbl.gremlin.ru", |
| "web.rbl.msrbl.net", |
| "work.drbl.caravan.ru", |
| "work.drbl.gremlin.ru", |
| "wormrbl.imp.ch", |
| "xbl.spamhaus.org", |
| "zen.spamhaus.org", |
| ] |
| is_blacklist = False |
| blacklist = [] |
| for service in blacklist_services: |
| try: |
| |
| query_domain = domain + "." + service |
| response = dns.resolver.resolve(query_domain, "A") |
|
|
| |
| if response: |
| is_blacklist = True |
| blacklist.append(service) |
|
|
| except Exception as e: |
| pass |
| |
|
|
| data = { |
| "is_blacklist": is_blacklist, |
| "blacklist": blacklist, |
| } |
|
|
| return data |
|
|
| |
| def get_service_monitor_status(self, get): |
| c_id = public.M('crontab').where('name=?', u'[勿删] 邮局服务监控').getField('id') |
| if not c_id: return self.return_msg(public.returnMsg(False, '任务不存在!')) |
| data = public.M('crontab').where('name=?', u'[勿删] 邮局服务监控').find() |
| return self.return_msg(public.returnMsg(True, data)) |
|
|
| |
| def create_service_monitor_task(self, get): |
| import crontab |
| p = crontab.crontab() |
|
|
| try: |
|
|
| c_id = public.M('crontab').where('name=?', u'[勿删] 邮局服务监控').getField('id') |
| if c_id: |
| data = {} |
| data['id'] = c_id |
| data['name'] = u'[勿删] 邮局服务监控' |
| |
| |
| data['type'] = 'minute-n' |
| data['where1'] = '1' |
| data['sBody'] = 'btpython /www/server/panel/class/mailModel/script/monitor_script.py' |
| data['backupTo'] = '' |
| data['sType'] = 'toShell' |
| |
| |
| |
| data['hour'] = '' |
| data['minute'] = '' |
| data['week'] = '' |
| data['sName'] = '' |
| data['urladdress'] = '' |
| data['save'] = '' |
| p.modify_crond(data) |
| return self.return_msg(public.returnMsg(True, '编辑成功!')) |
| else: |
| data = {} |
| data['name'] = u'[勿删] 邮局服务监控' |
| |
| |
| data['type'] = 'minute-n' |
| data['where1'] = '1' |
| data['sBody'] = 'btpython /www/server/panel/class/mailModel/script/monitor_script.py' |
| data['backupTo'] = '' |
| data['sType'] = 'toShell' |
| |
| |
| |
| data['hour'] = '' |
| data['minute'] = '' |
| data['week'] = '' |
| data['sName'] = '' |
| data['urladdress'] = '' |
| data['save'] = '' |
| p.AddCrontab(data) |
| return self.return_msg(public.returnMsg(True, '设置成功!')) |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
|
|
| |
| def open_service_monitor_task(self, get): |
| import crontab |
| p = crontab.crontab() |
|
|
| try: |
|
|
| c_id = public.M('crontab').where('name=?', u'[勿删] 邮局服务监控').getField('id') |
| if c_id: |
| data = {} |
| data['id'] = c_id |
| data['name'] = u'[勿删] 邮局服务监控' |
| |
| |
| data['type'] = 'minute-n' |
| data['where1'] = '1' |
| data['sBody'] = 'btpython /www/server/panel/class/mailModel/script/monitor_script.py' |
| data['backupTo'] = '' |
| data['sType'] = 'toShell' |
| |
| |
| |
| data['hour'] = '' |
| data['minute'] = '' |
| data['week'] = '' |
| data['sName'] = '' |
| data['urladdress'] = '' |
| data['save'] = '' |
| p.modify_crond(data) |
| return public.returnMsg(True, public.lang('修改成功!')) |
| else: |
| data = {} |
| data['name'] = u'[勿删] 邮局服务监控' |
| |
| |
| data['type'] = 'minute-n' |
| data['where1'] = '1' |
| data['sBody'] = 'btpython /www/server/panel/class/mailModel/script/monitor_script.py' |
| data['backupTo'] = '' |
| data['sType'] = 'toShell' |
| |
| |
| |
| data['hour'] = '' |
| data['minute'] = '' |
| data['week'] = '' |
| data['sName'] = '' |
| data['urladdress'] = '' |
| data['save'] = '' |
| p.AddCrontab(data) |
| return public.returnMsg(True, public.lang('设置成功!')) |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
|
|
| |
| def close_service_monitor_task(self, get): |
| import crontab |
|
|
| p = crontab.crontab() |
| c_id = public.M('crontab').where('name=?', u'[勿删] 邮局服务监控').getField('id') |
| if not c_id: return self.return_msg(public.returnMsg(False, '任务不存在!')) |
| args = {"id": c_id} |
| p.DelCrontab(args) |
| return self.return_msg(public.returnMsg(True, '关闭成功!')) |
|
|
| |
| def export_users(self, get): |
|
|
| rule_path = '/www/server/panel/data/mail/' |
| if not os.path.exists(rule_path): |
| os.makedirs(rule_path, exist_ok=True) |
|
|
| file_name = "All_users_{}".format(int(time.time())) |
| |
| query = self.M('mailbox').order('created desc').field( |
| 'full_name,is_admin,username,password,password_encode,maildir,quota,local_part,domain') |
|
|
| if hasattr(get, 'domain') and get.get('domain/s', '') != '': |
| domain = get.get('domain/s', '') |
| |
| file_name = "{}_users_{}".format(domain, int(time.time())) |
| query = self.M('mailbox').where('domain=?', domain).order('created desc').field( |
| 'full_name,is_admin,username,password,password_encode,maildir,quota,local_part,domain') |
|
|
| data_list = query.select() |
|
|
| if not data_list: |
| return self.return_msg(public.returnMsg(False, '没有用户可以导出')) |
|
|
| file_path = "{}{}.json".format(rule_path, file_name) |
| public.writeFile(file_path, public.GetJson(data_list)) |
|
|
| return self.return_msg(public.returnMsg(True, file_path)) |
|
|
| |
| def import_users(self, get): |
|
|
| get.file = get.get('file/s', '') |
|
|
| if not get.file: |
| return self.return_msg(public.returnMsg(False, '文件不能为空')) |
|
|
| if not os.path.exists(get.file): |
| return self.return_msg(self.return_msg(public.returnMsg(False, '文件不存在'))) |
|
|
| try: |
| data = public.readFile(get.file) |
| data = json.loads(data) |
| data.reverse() |
| except: |
| return self.return_msg(public.returnMsg(False, '文件内容有误或格式不正确')) |
| |
|
|
| create_successfully = {} |
| create_failed = {} |
|
|
| args = public.dict_obj() |
| for item in data: |
|
|
| if not item: |
| continue |
| if not item['username'] or not item['password']: |
| continue |
|
|
| try: |
| |
| args.full_name = item['full_name'] |
| args.is_admin = item['is_admin'] |
| args.username = item['username'] |
| args.password_encrypt = item['password'] |
| args.password_encode = item['password_encode'] |
| args.maildir = item['maildir'] |
| args.quota = item['quota'] |
| args.local_part = item['local_part'] |
| args.domain = item['domain'] |
| result = self._add_mailbox2(args) |
| if result['status']: |
| create_successfully[item['username']] = result['msg'] |
| continue |
| create_failed[item['username']] = result['msg'] |
| except Exception as ex: |
| public.print_log(traceback.format_exc()) |
| create_failed[item['username']] = "create error {}".format(ex) |
|
|
| return self.return_msg({'status': True, 'msg': "导入邮箱[{}]成功".format(','.join(create_successfully)), |
| 'error': create_failed, |
| 'success': create_successfully}) |
|
|
| |
| def _add_mailbox2(self, args): |
| ''' |
| 新增邮箱用户 取消存储空间字节转换 取消密码加密(存的就是加密的) |
| :param args: |
| :return: |
| ''' |
|
|
| username = args.username |
| |
| |
| if not username.islower(): |
| return public.returnMsg(False, '电子邮件地址不能有大写字母!') |
| is_admin = args.is_admin if 'is_admin' in args else 0 |
|
|
| local_part, domain = username.split('@') |
| |
| user_count = self.M('mailbox').where('domain=?', (args.domain,)).count() |
| domaincount = self.M('domain').where('domain=?', (args.domain,)).getField("mailboxes") |
| if user_count + 1 > domaincount: |
| return public.returnMsg(False,'{}的邮箱数量已达到限制{}'.format(args.domain, domaincount)) |
|
|
| domain_list = [item['domain'] for item in self.M('domain').field('domain').select()] |
| if domain not in domain_list: |
| return public.returnMsg(False, '不存在该域名 {}'.format(domain)) |
|
|
| count = self.M('mailbox').where('username=?', (username,)).count() |
| if count > 0: |
| return public.returnMsg(False, 'EMAIL_EXIST') |
|
|
| cur_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
| self.M('mailbox').add( |
| 'full_name,is_admin,username,password,password_encode,maildir,quota,local_part,domain,created,modified', |
| (args.full_name, is_admin, username, args.password_encrypt, args.password_encode, args.username + '/', |
| args.quota, |
| local_part, args.domain, cur_time, cur_time)) |
|
|
| |
| user_path = '/www/vmail/{0}/{1}'.format(domain, local_part) |
| os.makedirs(user_path) |
| os.makedirs(user_path + '/tmp') |
| os.makedirs(user_path + '/new') |
| os.makedirs(user_path + '/cur') |
| public.ExecShell('chown -R vmail:mail /www/vmail/{0}/{1}'.format(domain, local_part)) |
| |
| password = self._decode(args.password_encode) |
| self.create_mail_box(username, password) |
| return public.returnMsg(True, "成功添加用户 {}".format(username)) |
|
|
| def check_field_exists(self, db_obj, table_name, field_name): |
| """ |
| @name 检查表字段是否存在 |
| @param db_obj 数据库对象 |
| @param table_name 表名 |
| @param field_name 要检查的字段 |
| """ |
| try: |
| res = db_obj.query("PRAGMA table_info({})".format(table_name)) |
| for val in res: |
| if field_name == val[1]: |
| return True |
| except: |
| pass |
| return False |
|
|
| |
| def check_domain_column(self, ): |
| """ |
| @name 检查数据库表或字段是否完整 |
| """ |
| with self.M("domain") as obj: |
| if not self.check_field_exists(obj, "domain", "a_record"): |
| obj.execute('ALTER TABLE `domain` ADD COLUMN `a_record` Text default "";') |
|
|
| if not self.check_field_exists(obj, "domain", "mailboxes"): |
| obj.execute('ALTER TABLE `domain` ADD COLUMN `mailboxes` INT DEFAULT 50;') |
|
|
| if not self.check_field_exists(obj, "domain", "mailbox_quota"): |
| obj.execute('ALTER TABLE `domain` ADD COLUMN `mailbox_quota` BIGINT(20) NOT NULL DEFAULT 5368709120;') |
|
|
| if not self.check_field_exists(obj, "domain", "quota"): |
| obj.execute('ALTER TABLE `domain` ADD COLUMN `quota` BIGINT(20) NOT NULL DEFAULT 10737418240;') |
|
|
| if not self.check_field_exists(obj, "domain", "rate_limit"): |
| obj.execute('ALTER TABLE `domain` ADD COLUMN `rate_limit` INT DEFAULT 12;') |
|
|
| sql2 = '''CREATE TABLE IF NOT EXISTS `email_task` ( |
| `id` INTEGER PRIMARY KEY AUTOINCREMENT, |
| `task_name` varchar(255) NOT NULL, -- 任务名 |
| `addresser` varchar(320) NOT NULL, -- 发件人 |
| `recipient_count` int NOT NULL, -- 收件人数量 |
| `task_process` tinyint NOT NULL, -- 任务进程 0待执行 1执行中 2 已完成 |
| `pause` tinyint NOT NULL, -- 暂停状态 1 暂停中 0 未暂停 执行中的任务才能暂停 |
| `temp_id` INTEGER NOT NULL, -- 邮件对应id |
| `is_record` INTEGER NOT NULL DEFAULT 0, -- 是否记录到发件箱 |
| `unsubscribe` INTEGER NOT NULL DEFAULT 0, -- 是否增加退订按钮 0 没有 1 增加退订按钮 |
| `threads` INTEGER NOT NULL DEFAULT 0, -- 线程数量 控制发送线程数 0时自动控制线程 0~10 |
| `created` INTEGER NOT NULL, |
| `modified` INTEGER NOT NULL, |
| `active` tinyint(1) NOT NULL DEFAULT 0 -- 预留字段 |
| );''' |
| with self.M("") as obj: |
| obj.execute(sql2, ()) |
| |
|
|
| |
| if os.path.exists('/www/vmail'): |
| sql = '''CREATE TABLE IF NOT EXISTS `mail_errlog` ( |
| `id` INTEGER PRIMARY KEY AUTOINCREMENT, |
| `created` INTEGER NOT NULL, -- 收件人 |
| `recipient` varchar(320) NOT NULL, -- 收件人 |
| `delay` varchar(320) NOT NULL, -- 延时 |
| `delays` varchar(320) NOT NULL, -- 各阶段延时 |
| `dsn` varchar(320) NOT NULL, -- dsn |
| `relay` text NOT NULL, -- 中继服务器 |
| `domain` varchar(320) NOT NULL, -- 域名 |
| `status` varchar(255) NOT NULL, -- 错误状态 |
| `err_info` text NOT NULL, -- 错误详情 |
| UNIQUE(created, recipient) |
| );''' |
|
|
| with self.MD("", "postfixmaillog") as obj2: |
| obj2.execute(sql, ()) |
|
|
| |
| sql = '''CREATE TABLE IF NOT EXISTS `mail_unsubscribe` ( |
| `id` INTEGER PRIMARY KEY AUTOINCREMENT, |
| `created` INTEGER NOT NULL, |
| `recipient` varchar(320) NOT NULL, -- 收件人 |
| `etype` INTEGER NOT NULL DEFAULT 1, -- 邮件类型id |
| `active` tinyint(1) NOT NULL DEFAULT 0, -- 0 取消订阅 1订阅 |
| `task_id` INTEGER DEFAULT 0, -- 群发任务 id (退订有关联id 订阅没有) |
| UNIQUE(etype, recipient) |
| );''' |
|
|
| with self.MD("", "mail_unsubscribe") as obj3: |
| aa = obj3.execute(sql, ()) |
| |
|
|
| |
| sql = '''CREATE TABLE IF NOT EXISTS `abnormal_recipient` ( |
| `id` INTEGER PRIMARY KEY AUTOINCREMENT, |
| `created` INTEGER NOT NULL, -- 邮件时间 时间戳 |
| `recipient` varchar(320) NOT NULL, -- 收件人 |
| `count` INTEGER NOT NULL, -- 次数 |
| `status` varchar(255) NOT NULL, -- 状态 |
| `task_name` varchar(255) NOT NULL, -- 任务名 |
| UNIQUE(recipient) |
| );''' |
|
|
| with self.MD("", "abnormal_recipient") as obj4: |
| obj4.execute(sql, ()) |
|
|
| |
| sql = '''CREATE TABLE IF NOT EXISTS `log_analysis` ( |
| `id` INTEGER PRIMARY KEY AUTOINCREMENT, |
| `received` INTEGER NOT NULL DEFAULT 0, -- 接收 |
| `delivered` INTEGER NOT NULL DEFAULT 0, -- 发送 |
| `deferred` INTEGER NOT NULL DEFAULT 0, -- 延迟 |
| `bounced` INTEGER NOT NULL DEFAULT 0, -- 退回 |
| `rejected` INTEGER NOT NULL DEFAULT 0, -- 拒绝 |
| `time` INTEGER NOT NULL, -- 时间 每小时时间戳 |
| UNIQUE(`time`) |
| );''' |
| with self.M("") as obj: |
| obj.execute(sql, ()) |
|
|
| |
| mail_type_table_str = self.M('sqlite_master').where('type=? AND name=?', ('table', 'mail_type')).find() |
| if not mail_type_table_str: |
| |
| sql = '''CREATE TABLE IF NOT EXISTS `mail_type` ( |
| `id` INTEGER PRIMARY KEY AUTOINCREMENT, |
| `mail_type` varchar(320) NOT NULL, -- 邮件类型 |
| `created` INTEGER NOT NULL, |
| `active` tinyint(1) NOT NULL DEFAULT 0 -- 预留字段 |
| );''' |
| with self.M("") as obj: |
| obj.execute(sql, ()) |
|
|
| |
| sql_insert = ''' INSERT INTO `mail_type`(`mail_type`, `created`) VALUES ('Default', strftime('%s', 'now'));''' |
| with self.M("") as obj: |
| obj.execute(sql_insert, ()) |
|
|
| def _convert_quota_to_bytes(self, quota): |
| num, unit = quota.split() |
| if unit == 'GB': |
| quota = float(num) * 1024 * 1024 * 1024 |
| else: |
| quota = float(num) * 1024 * 1024 |
| return quota |
|
|
| |
| def add_domain_new(self, args): |
| ''' |
| 域名增加接口 |
| :param args: |
| :return: |
| ''' |
| if 'domain' not in args: |
| return self.return_msg(public.returnMsg(False, 'DOMAIN_NAME')) |
| domain = args.domain |
| a_record = args.a_record |
| |
| |
| |
| |
| |
|
|
| if self.M('domain').where('domain=?', domain).count() > 0: |
| return self.return_msg(public.returnMsg(False, '域名已存在')) |
| |
| if not hasattr(args, 'mailboxes') or args.get('mailboxes/d', 0) == 0: |
| args.mailboxes = 50 |
| if not hasattr(args, 'mailbox_quota') or args.get('mailbox_quota/s', "") == "": |
| args.mailbox_quota = "5 GB" |
| if not hasattr(args, 'quota') or args.get('quota/s', "") == "": |
| args.quota = "10 GB" |
| if not hasattr(args, 'rate_limit') or args.get('rate_limit/d', 0) == 0: |
| args.rate_limit = 12 |
|
|
| mailboxes = args.mailboxes |
| rate_limit = args.rate_limit |
| mailbox_quota = self._convert_quota_to_bytes(args.mailbox_quota) |
| quota = self._convert_quota_to_bytes(args.quota) |
|
|
| |
| cur_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
| try: |
| self.M('domain').add('domain,a_record,mailboxes,mailbox_quota,quota,rate_limit,created', |
| (domain, a_record, mailboxes, mailbox_quota, quota, rate_limit, cur_time)) |
| except: |
| return self.return_msg(public.returnMsg(False, '邮件服务器初始化失败。请重新打开插件以进行初始化<br>' |
| '如果服务器没有打开25 端口(出站方向),将无法完成初始化。<br>' |
| '您可以在终端中运行以下命令检查端口是否打开:<br><br>[ telnet gmail-smtp-in.l.google.com 25 ]')) |
|
|
| errip = [] |
| |
| if 'ips' in args: |
| data = {domain: {"ipv4": [], "ipv6": []}} |
| ips = args.ips |
| |
| ip_list = ips.split(';') |
| |
| for ip in ip_list: |
| if public.is_ipv4(ip): |
| data[domain]["ipv4"].append(ip) |
| elif public.is_ipv6(ip): |
| data[domain]["ipv6"].append(ip) |
| else: |
| errip.append(ip) |
|
|
| |
| path = '/www/server/panel/plugin/mail_sys/domain_ip.json' |
|
|
| if not os.path.exists(path): |
| public.writeFile(path, json.dumps(data)) |
| else: |
| rdata = public.readFile(path) |
| try: |
| rdata = json.loads(rdata) |
| except: |
| pass |
| rdata.update(data) |
| public.writeFile(path, json.dumps(rdata)) |
| |
| if hasattr(args, 'email') and args.get('email/s', "") != "": |
| self._add_enable_catchall(args) |
| |
| if not os.path.exists('/www/vmail/{0}'.format(domain)): |
| os.makedirs('/www/vmail/{0}'.format(domain)) |
| public.ExecShell('chown -R vmail:mail /www/vmail/{0}'.format(domain)) |
|
|
| |
| dns_id = 0 |
| if "dns_id" in args: |
| dns_id = args.dns_id |
| |
| from sslModel.base import sslBase |
| root_domain, _, _ = sslBase().extract_zone(domain) |
| |
| dns_data = public.M('ssl_domains').where("domain=?", (root_domain,)).find() |
| if not dns_data: |
| public.M('ssl_domains').add('domain,dns_id,type_id,endtime,ps', (domain, dns_id, 0, 0, '')) |
| |
| if 'auto_create_record' in args and args.auto_create_record: |
| self.auto_create_dns_record(args) |
| if 'ip_tag' in args and args.ip_tag: |
| from mailModel import multipleipModel |
| ip_tags = args.ip_tag.split(',') |
| bind = "@{}".format(domain) |
| multipleipModel.main().add_bind_ip_tag(ip_tags[0], bind) |
| if len(ip_tags) > 1: |
| multipleipModel.set_ip_rotate_conf(domain, ip_tags, 10, True) |
|
|
| if len(errip) > 0: |
| return self.return_msg(public.returnMsg(True, '域名[{}]添加成功! ip err:{}'.format(domain, errip))) |
| return self.return_msg(public.returnMsg(True, '域名[{0}]添加成功!'.format(domain))) |
|
|
| def update_domain(self, args): |
| ''' |
| 域名编辑接口 |
| :param args: |
| :return: |
| ''' |
| if 'domain' not in args: |
| return self.return_msg(public.returnMsg(False, 'DOMAIN_NAME')) |
|
|
| domain = args.domain |
| if self.M('domain').where('domain=?', domain).count() == 0: |
| return self.return_msg(public.returnMsg(False, '域名不存在')) |
|
|
| if not hasattr(args, 'rate_limit') or args.get('rate_limit/d', 0) == 0: |
| args.rate_limit = 12 |
| if not hasattr(args, 'mailboxes') or args.get('mailboxes/d', 0) == 0: |
| args.mailboxes = 50 |
| if not hasattr(args, 'mailbox_quota') or args.get('mailbox_quota/s', "") == "": |
| args.mailbox_quota = "5 GB" |
| if not hasattr(args, 'quota') or args.get('quota/s', "") == "": |
| args.quota = "10 GB" |
|
|
| rate_limit = args.rate_limit |
| mailboxes = args.mailboxes |
| mailbox_quota = self._convert_quota_to_bytes(args.mailbox_quota) |
| quota = self._convert_quota_to_bytes(args.quota) |
|
|
| try: |
| data = { |
| "mailboxes": mailboxes, |
| "mailbox_quota": mailbox_quota, |
| "quota": quota, |
| "rate_limit": rate_limit, |
| } |
| self.M('domain').where('domain=?', domain).update(data) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
|
|
| |
| if hasattr(args, 'email'): |
| email_old = self._get_domain_forward(domain) |
| if args.get('email/s', "") == "": |
| self.enable_catchall(args) |
| else: |
| if email_old != args.email: |
| self.enable_catchall(args) |
|
|
| |
| dns_id = 0 |
| if "dns_id" in args: |
| dns_id = args.dns_id |
| |
| from sslModel.base import sslBase |
| root_domain, _, _ = sslBase().extract_zone(domain) |
| |
| dns_data = public.M('ssl_domains').where("domain=?", (root_domain,)).find() |
| if not dns_data: |
| public.M('ssl_domains').add('domain,dns_id,type_id,endtime,ps', (domain, dns_id, 0, 0, '')) |
| else: |
| public.M('ssl_domains').where("domain=?", (root_domain,)).update({'dns_id': dns_id}) |
| |
| if 'auto_create_record' in args and args.auto_create_record: |
| self.auto_create_dns_record(args) |
| if 'ip_tag' in args: |
| from mailModel import multipleipModel |
| ip_tags = args.ip_tag.split(',') |
| bind = "@{}".format(domain) |
| multipleipModel_main = multipleipModel.main() |
| multipleipModel_main.edit_bind_ip_tag(ip_tags[0], bind) |
| if len(ip_tags) > 1: |
| multipleipModel_main.set_ip_rotate_conf(domain, ip_tags) |
| else: |
| if not ip_tags[0]: ip_tags = [] |
| multipleipModel_main.set_ip_rotate_conf(domain, ip_tags, status=False) |
|
|
| return self.return_msg(public.returnMsg(True, '修改域名[{}]成功!'.format(domain))) |
|
|
| |
| def _deledte_catchall(self, domain): |
| ''' |
| 删除邮件被转发 |
| :param args: |
| :return: |
| ''' |
| domain = '@' + domain.strip() |
| self.M('alias').where('address=? or address=?', (domain, '%'+domain)).delete() |
|
|
| |
| def _mail_logs_task(self, args): |
|
|
| import crontab |
| p = crontab.crontab() |
| try: |
| c_id = public.M('crontab').where('name=?', u'[勿删] 邮件日志').getField('id') |
|
|
| if not c_id: |
| data = {} |
| data['name'] = u'[勿删] 邮件日志' |
| data['type'] = 'minute-n' |
| data['where1'] = '10' |
| data['sBody'] = 'btpython /www/server/panel/class/mailModel/script/mail_logs.py' |
| data['backupTo'] = '' |
| data['sType'] = 'toShell' |
| data['hour'] = '' |
| data['minute'] = '' |
| data['week'] = '' |
| data['sName'] = '' |
| data['urladdress'] = '' |
| data['save'] = '' |
| p.AddCrontab(data) |
| return self.return_msg(public.returnMsg(True, '设置成功!')) |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
|
|
| |
| def mail_log_list(self, args): |
| |
| p = int(args.p) if 'p' in args else 1 |
| rows = int(args.size) if 'size' in args else 10 |
| callback = args.callback if 'callback' in args else '' |
| try: |
| count = self.M('email_log').count() |
| |
| page_data = public.get_page(count, p, rows, callback) |
| pattern = r"href='(?:/v2)?/plugin.*?\?p=(\d+)'" |
| |
| page_data['page'] = re.sub(pattern, r"href='\1'", page_data['page']) |
| |
| data_list = self.M('email_log').order('created desc').limit( |
| page_data['shift'] + ',' + page_data['row']).select() |
| |
| return self.return_msg({'data': data_list, 'page': page_data['page']}) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
|
|
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| def add_roundcube(self, args): |
| public.set_module_logs('mailModel', 'add_roundcube', 1) |
| is_ok = self.get_roundcube_status(None) |
| if isinstance(is_ok, dict) and is_ok['status']: |
| return self.return_msg(public.returnMsg(False, "roundcube已存在")) |
|
|
| if not os.path.exists('/usr/bin/mysql'): |
| return self.return_msg(public.returnMsg(False, '没有检测到MySQL服务!请先安装MySQL')) |
| |
| from panelModel.publicModel import main |
| get1 = public.dict_obj() |
| get1.name = 'mysql' |
| mysqlinfo = main().get_soft_status(get1) |
| public.print_log("mysqlinfo--{}".format(mysqlinfo)) |
| if not mysqlinfo['status']: |
| public.print_log(mysqlinfo['status']) |
| if not mysqlinfo['setup'] or not mysqlinfo['status']: |
| return self.return_msg(public.returnMsg(False, '没有检测到MySQL服务!请先安装MySQL')) |
|
|
| |
| webserver = public.GetWebServer() |
| if webserver == 'nginx': |
| |
| if not os.path.exists('/etc/init.d/nginx'): |
| return self.return_msg(public.returnMsg(False, '没有检测到nginx服务!请先安装nginx')) |
| get2 = public.dict_obj() |
| get2.name = 'nginx' |
| mysqlinfo = main().get_soft_status(get2) |
| if not mysqlinfo['status']: |
| if not mysqlinfo['setup'] or not mysqlinfo['status']: |
| return self.return_msg(public.returnMsg(False, '没有检测到nginx服务!请先安装nginx')) |
|
|
| args.dname = 'roundcube' |
| if not hasattr(args, 'site_name') or args.get('site_name/s', "") == "": |
|
|
| return self.return_msg(public.returnMsg(False, '参数错误 site_name')) |
| if not hasattr(args, 'php_version') or args.get('php_version/s', "") == "": |
| return self.return_msg(public.returnMsg(False, '参数错误 php_version')) |
| site_name = args.site_name |
| php_version = args.php_version |
|
|
| |
| from panelSite import panelSite |
| |
| ps = site_name.replace('.', '_').replace('-', '_') |
| data = panelSite().AddSite(public.to_dict_obj({ |
| 'webname': json.dumps({ |
| 'domain': site_name, |
| 'domainlist': [], |
| 'count': 0, |
| }), |
| 'type': 'PHP', |
| 'version': php_version, |
| 'port': '80', |
| 'path': '/www/wwwroot/' + site_name, |
| 'sql': 'MySQL', |
| 'datauser': 'sql_' + ps, |
| 'datapassword': public.GetRandomString(16).lower(), |
| 'codeing': 'utf8mb4', |
| 'ps': ps, |
| 'set_ssl': 0, |
| 'force_ssl': 0, |
| 'ftp': False, |
| })) |
| |
| public.print_log(data) |
| if not data.get('status', True): |
| return self.return_msg(data) |
|
|
| deployment = self.SetupPackage_roundcube(args) |
| if not deployment['status']: |
| return self.return_msg(deployment) |
|
|
| tistamp = int(time.time()) |
| |
| roundcube_info = { |
| "status": True, |
| "id": data['siteId'], |
| "site_name": site_name, |
| "php_version": php_version, |
| "ssl_status": False, |
| |
| "timestimp": tistamp, |
| } |
| path = "/www/server/panel/plugin/mail_sys/roundcube.json" |
| public.writeFile(path, json.dumps(roundcube_info)) |
| return self.return_msg(public.returnMsg(True, '安装成功')) |
|
|
| def SetupPackage_roundcube(self, get): |
| import plugin_deployment |
| sysObject = plugin_deployment.plugin_deployment() |
|
|
| name = get.dname |
| site_name = get.site_name |
| php_version = get.php_version |
| |
| find = public.M('sites').where('name=?', (site_name,)).field('id,path').find() |
| path = find['path'] |
|
|
| pinfo = { |
| "username": "", |
| "ps": "免费开源的邮件客户端程序", |
| "php": "56,70,71,72,73,74,80", |
| "run": "", |
| "name": "roundcube", |
| "title": "Roundcube", |
| "type": 6, |
| "chmod": "", |
| "ext": "pathinfo,exif", |
| "version": "1.5.0", |
| "install": "", |
| |
| |
| "download": "https://node.aapanel.com/install/package/roundcubemail.zip", |
| |
| "password": "", |
| "config": "/config/config.inc.php", |
| "md5": "785660db6540692b5c0eb240b41816e9" |
| } |
|
|
| |
| sysObject.WriteLogs( |
| json.dumps({'name': public.GetMsg("VERIFYING_PACKAGE"), 'total': 0, 'used': 0, 'pre': 0, 'speed': 0})) |
| |
| packageZip = 'plugin/mail_sys/' + name + '.zip' |
| isDownload = False |
| if os.path.exists(packageZip): |
| md5str = sysObject.GetFileMd5(packageZip) |
| if md5str != pinfo['md5']: |
| isDownload = True |
| else: |
| isDownload = True |
|
|
| |
| rm_file = path + '/index.html' |
| if os.path.exists(rm_file): os.remove(rm_file) |
|
|
| |
| if isDownload: |
| sysObject.WriteLogs( |
| json.dumps({'name': public.GetMsg("DOWNLOAD"), 'total': 0, 'used': 0, 'pre': 0, 'speed': 0})) |
| sysObject.DownloadFile(pinfo['download'], packageZip) |
|
|
| if not os.path.exists(packageZip): |
| return public.returnMsg(False, "DOWNLOAD_FILE_FAIL") |
|
|
| sysObject.WriteLogs(json.dumps({'name': public.GetMsg("UNPACKING"), 'total': 0, 'used': 0, 'pre': 0, 'speed': 0})) |
| public.ExecShell('unzip -o ' + packageZip + ' -d ' + path + '/') |
|
|
| |
| sysObject.WriteLogs( |
| json.dumps({'name': public.GetMsg("SET_PERMISSION"), 'total': 0, 'used': 0, 'pre': 0, 'speed': 0})) |
| public.ExecShell('chmod -R 755 ' + path) |
| public.ExecShell('chown -R www.www ' + path) |
|
|
| if pinfo['chmod'] != "": |
| access = pinfo['chmod'].split(',') |
| for chm in access: |
| tmp = chm.split('|') |
| if len(tmp) != 2: continue; |
| public.ExecShell('chmod -R ' + tmp[0] + ' ' + path + '/' + tmp[1]) |
|
|
| |
| sysObject.WriteLogs( |
| json.dumps({'name': public.GetMsg("EXECUTE_EXTRA_SHELL"), 'total': 0, 'used': 0, 'pre': 0, 'speed': 0})) |
| if os.path.exists(path + '/install.sh'): |
| public.ExecShell('cd ' + path + ' && bash ' + 'install.sh') |
| public.ExecShell('rm -f ' + path + '/install.sh') |
|
|
| |
| if os.path.exists(path + '/composer.json'): |
| sysObject.WriteLogs(json.dumps({'name': 'Execute Composer', 'total': 0, 'used': 0, 'pre': 0, 'speed': 0})) |
| if not os.path.exists(path + '/composer.lock'): |
| execPHP = '/www/server/php/' + php_version + '/bin/php' |
| if execPHP: |
| if public.get_url().find('125.88'): |
| public.ExecShell( |
| 'cd ' + path + ' && ' + execPHP + ' /usr/bin/composer config repo.packagist composer https://packagist.phpcomposer.com') |
| import panelSite |
| phpini = '/www/server/php/' + php_version + '/etc/php.ini' |
| phpiniConf = public.readFile(phpini) |
| phpiniConf = phpiniConf.replace('proc_open,proc_get_status,', '') |
| public.writeFile(phpini, phpiniConf) |
| public.ExecShell( |
| 'nohup cd ' + path + ' && ' + execPHP + ' /usr/bin/composer install -vvv > /tmp/composer.log 2>&1 &') |
|
|
| |
| sysObject.WriteLogs( |
| json.dumps({'name': public.GetMsg("SET_URL_REWRITE"), 'total': 0, 'used': 0, 'pre': 0, 'speed': 0})) |
| swfile = path + '/nginx.rewrite' |
| if os.path.exists(swfile): |
| rewriteConf = public.readFile(swfile) |
| dwfile = sysObject.__panelPath + '/vhost/rewrite/' + site_name + '.conf' |
| public.writeFile(dwfile, rewriteConf) |
|
|
| |
| public.ExecShell("rm -f " + path + '/*.rewrite') |
|
|
| |
| sysObject.WriteLogs(json.dumps({'name': public.GetMsg("SET_RUN_DIR"), 'total': 0, 'used': 0, 'pre': 0, 'speed': 0})) |
| if pinfo['run'] != '/': |
| import panelSite |
| from plugin_deployment import obj |
| siteObj = panelSite.panelSite() |
| mobj = obj() |
| mobj.id = find['id'] |
| mobj.runPath = pinfo['run'] |
| |
| siteObj.SetSiteRunPath(mobj) |
|
|
| |
| sysObject.WriteLogs(json.dumps({'name': public.GetMsg("IMPORT_DB"), 'total': 0, 'used': 0, 'pre': 0, 'speed': 0})) |
|
|
| if os.path.exists(path + '/import.sql'): |
| databaseInfo = public.M('databases').where('pid=?', (find['id'],)).field('username,password').find() |
| if databaseInfo: |
| public.ExecShell('/www/server/mysql/bin/mysql -u' + databaseInfo['username'] + ' -p' + databaseInfo[ |
| 'password'] + ' ' + databaseInfo['username'] + ' < ' + path + '/import.sql') |
|
|
| public.ExecShell('rm -f ' + path + '/import.sql') |
| |
|
|
| siteConfigFile = path + '/' + pinfo['config'] |
| if os.path.exists(siteConfigFile): |
|
|
| siteConfig = public.readFile(siteConfigFile) |
| siteConfig = siteConfig.replace('BT_DB_USERNAME', databaseInfo['username']) |
| siteConfig = siteConfig.replace('BT_DB_PASSWORD', databaseInfo['password']) |
| siteConfig = siteConfig.replace('BT_DB_NAME', databaseInfo['username']) |
| |
| public.writeFile(siteConfigFile, siteConfig) |
|
|
|
|
| public.serviceReload() |
| sysObject.depTotal(name) |
| sysObject.WriteLogs( |
| json.dumps({'name': public.GetMsg("READY_DEPLOY"), 'total': 0, 'used': 0, 'pre': 0, 'speed': 0})) |
|
|
| return public.returnMsg(True, pinfo) |
|
|
| |
| def _get_roundcube_ssl(self, site_name): |
| from data import data |
| has_ssl = data().get_site_ssl_info(site_name) |
| if has_ssl != -1: |
| return True |
| else: |
| return False |
|
|
| |
| def get_roundcube_status(self, args): |
|
|
| |
| |
| |
| |
|
|
| path = "/www/server/panel/plugin/mail_sys/roundcube.json" |
| if os.path.exists(path): |
| data = public.readFile(path) |
| public_data = {} |
| if data != '': |
| public_data = json.loads(data) |
|
|
| return self.return_msg(public_data) |
| else: |
| return self.return_msg({"status": False}) |
|
|
| |
| def _roundcube_ssl_status(self): |
| path = "/www/server/panel/plugin/mail_sys/roundcube.json" |
|
|
| if os.path.exists(path): |
| data = public.readFile(path) |
| |
| if data != '': |
| public_data = json.loads(data) |
|
|
| site_name = public_data['site_name'] |
| |
| |
| public_data['ssl_status'] = True if self._get_multiple_certificate_domain_status( |
| site_name) or self._get_roundcube_ssl(site_name) else False |
| public.writeFile(path, json.dumps(public_data)) |
|
|
| def get_domain(self, args): |
| ''' |
| 查询网站 |
| :param args: |
| :return: |
| ''' |
|
|
| data_list = public.M('sites').field('id,name,path').select() |
| return self.return_msg(data_list) |
|
|
| |
| def add_roundcube_info(self, args): |
| if not hasattr(args, 'id') or args.get('id/d', 0) == 0: |
| return self.return_msg(public.returnMsg(False, '参数错误 id')) |
| if not hasattr(args, 'site_name') or args.get('site_name/s', "") == "": |
| return self.return_msg(public.returnMsg(False, '参数错误 site_name')) |
| if not hasattr(args, 'path') or args.get('path/s', "") == "": |
| return self.return_msg(public.returnMsg(False, '参数错误 path')) |
|
|
| id = args.get('id/d', 0) |
| name = args.get('site_name/s', "") |
| path = args.get('path/s', "") |
| |
| if path.endswith('/'): |
| path = path.rstrip('/') |
| cmp_path = path + '/composer.json' |
| if not os.path.exists(cmp_path): |
| return self.return_msg(public.returnMsg(False, '此站点未部署roundcube')) |
| info = json.loads(public.readFile(cmp_path)) |
| |
| if info['name'].find("roundcube") == -1: |
| return self.return_msg(public.returnMsg(False, '此站点未部署roundcube')) |
|
|
| tistamp = int(time.time()) |
| |
| roundcube_info = { |
| "status": True, |
| "id": id, |
| "site_name": name, |
| "php_version": None, |
| "ssl_status": self._get_multiple_certificate_domain_status(name), |
| "timestimp": tistamp, |
| } |
| path = "/www/server/panel/plugin/mail_sys/roundcube.json" |
| public.writeFile(path, json.dumps(roundcube_info)) |
| return self.return_msg(public.returnMsg(True, '添加成功')) |
|
|
|
|
| def uninstall_roundcube(self, args): |
|
|
| if not hasattr(args, 'site_name') or args.get('site_name/s', "") == "": |
| return self.return_msg(public.returnMsg(False, '参数 site_name 错误')) |
| if not hasattr(args, 'id') or args.get('id/s', "") == "": |
| return self.return_msg(public.returnMsg(False, '参数 id 错误')) |
| if not hasattr(args, 'force') or args.get('force/d', 0) == 0: |
| args.force = 0 |
| site_name = args.site_name |
| id = args.id |
| force = args.force |
| from panelSite import panelSite |
| if force: |
| data = panelSite().DeleteSite(public.to_dict_obj({ |
| 'id': id, |
| 'webname': site_name, |
| 'ftp': '1', |
| 'path': '1', |
| 'database': '1', |
| })) |
| else: |
| data = panelSite().DeleteSite(public.to_dict_obj({ |
| 'id': id, |
| 'webname': site_name, |
| })) |
| path = "/www/server/panel/plugin/mail_sys/roundcube.json" |
| if os.path.exists(path): |
| os.remove(path) |
| return self.return_msg(data) |
|
|
| |
| def recipient_blacklist_open(self, status): |
| |
| result = public.readFile(self.postfix_main_cf) |
| |
| if not result: |
| return False |
| match = re.search(r"smtpd_recipient_restrictions\s*=\s*(.+)", result) |
| if not match: |
| return False |
|
|
| if status: |
| new_restrictions = 'check_recipient_access hash:/etc/postfix/blacklist,permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination' |
| updated_config = re.sub( |
| r"smtpd_recipient_restrictions\s*=\s*(.+)", |
| f"smtpd_recipient_restrictions = {new_restrictions}", |
| result |
| ) |
| public.writeFile(self.postfix_main_cf, updated_config) |
| else: |
| new_restrictions = 'permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination' |
| updated_config = re.sub( |
| r"smtpd_recipient_restrictions\s*=\s*(.+)", |
| f"smtpd_recipient_restrictions = {new_restrictions}", |
| result |
| ) |
| public.writeFile(self.postfix_main_cf, updated_config) |
| return True |
|
|
| |
| def _recipient_blacklist_status(self): |
| |
| result = public.readFile(self.postfix_main_cf) |
|
|
| match = re.search(r"smtpd_recipient_restrictions\s*=\s*(.+)", result) |
| if not match: |
| return False |
|
|
| restrictions = match.group(1) |
| if 'check_recipient_access hash:/etc/postfix/blacklist' not in restrictions: |
| return False |
| else: |
| return True |
|
|
| |
| def recipient_blacklist(self, args): |
| keyword = args.get('keyword/s', '') |
|
|
| if not keyword or keyword == '': |
| keyword = None |
|
|
| |
| if not self._recipient_blacklist_status(): |
| |
| return self.return_msg(public.returnMsg(True, [])) |
|
|
|
|
| |
| if not os.path.exists(self.postfix_recipient_blacklist): |
| public.writeFile(self.postfix_recipient_blacklist, '') |
| public.ExecShell('postmap /etc/postfix/blacklist') |
|
|
| try: |
| with open(self.postfix_recipient_blacklist, 'r') as file: |
| emails = file.read().splitlines() |
| except Exception as e: |
| emails = [] |
|
|
| |
| if emails: |
| emails = [email.split()[0] for email in emails] |
| else: |
| |
| st = self.recipient_blacklist_open(False) |
| if st: |
| public.ExecShell('systemctl reload postfix') |
| return self.return_msg(public.returnMsg(True, [])) |
|
|
| |
| if keyword: |
| emails = [email for email in emails if re.search(keyword, email)] |
|
|
| return self.return_msg(public.returnMsg(True, emails)) |
|
|
| |
| def add_recipient_blacklist(self, args): |
| |
| if not os.path.exists(self.postfix_recipient_blacklist): |
| public.writeFile(self.postfix_recipient_blacklist, '') |
|
|
| emails_to_add = args.emails_to_add if 'emails_to_add' in args else [] |
| try: |
| emails_to_add = json.loads(args.emails_to_add) |
| except: |
| pass |
|
|
| try: |
|
|
| if not emails_to_add: |
| return self.return_msg(public.returnMsg(False, '参数错误emails_to_add')) |
|
|
| |
| add_set = {f"{email} REJECT\n" for email in emails_to_add} |
|
|
| try: |
| |
| with open(self.postfix_recipient_blacklist, 'r') as file: |
| existing_lines = set(file.readlines()) |
|
|
| |
| new_lines = add_set - existing_lines |
|
|
| |
| if new_lines: |
| with open(self.postfix_recipient_blacklist, 'a') as file: |
| file.writelines(new_lines) |
|
|
| except Exception as e: |
| return self.return_msg(public.returnMsg(False, e)) |
|
|
| |
| if not self._recipient_blacklist_status(): |
| |
| self.recipient_blacklist_open(True) |
|
|
| shell_str = ''' |
| postmap /etc/postfix/blacklist |
| systemctl reload postfix |
| ''' |
| public.ExecShell(shell_str) |
| except: |
| public.print_log(public.get_error_info()) |
|
|
| return self.return_msg(public.returnMsg(True, '成功添加黑名单')) |
|
|
| |
| def del_recipient_blacklist(self, args): |
| try: |
| emails_to_remove = json.loads(args.emails_to_remove) if 'emails_to_remove' in args else [] |
|
|
| if not emails_to_remove: |
| return self.return_msg(public.returnMsg(False, '参数错误 emails_to_remove')) |
|
|
| remove_set = {f"{email} REJECT\n" for email in emails_to_remove} |
|
|
| try: |
| |
| with open(self.postfix_recipient_blacklist, 'r') as file: |
| lines = file.readlines() |
|
|
| |
| with open(self.postfix_recipient_blacklist, 'w') as file: |
| for line in lines: |
| if line not in remove_set: |
| file.write(line) |
|
|
| except Exception as e: |
| return self.return_msg(public.returnMsg(False, e)) |
|
|
| |
| |
| |
| filedata = public.readFile(self.postfix_recipient_blacklist) |
| if not filedata or filedata == '': |
| self.recipient_blacklist_open(False) |
|
|
| shell_str = ''' |
| postmap /etc/postfix/blacklist |
| systemctl reload postfix |
| ''' |
| public.ExecShell(shell_str) |
| except: |
| public.print_log(public.get_error_info()) |
| return self.return_msg(public.returnMsg(True, '黑名单删除成功')) |
|
|
| |
| def export_recipient_blacklist(self, args): |
|
|
| |
| if not os.path.exists(self.postfix_recipient_blacklist): |
| return self.return_msg(public.returnMsg(False, '没有黑名单文件')) |
|
|
| try: |
| with open(self.postfix_recipient_blacklist, 'r') as file: |
| emails = file.read().splitlines() |
| except Exception as e: |
| emails = [] |
|
|
| |
| if emails != []: |
| emails = [email.split()[0] for email in emails] |
| else: |
| return self.return_msg(public.returnMsg(False, '没有黑名单可以导出')) |
| file_name = 'recipient_blacklist' |
| rule_path = '/www/server/panel/data/mail/' |
| file_path = "{}{}.json".format(rule_path, file_name) |
| public.writeFile(file_path, public.GetJson(emails)) |
| return self.return_msg(public.returnMsg(True, file_path)) |
|
|
| |
| def import_recipient_blacklist(self, args): |
| try: |
| file = args.get('file/s', '') |
|
|
| if not file: |
| return self.return_msg(public.returnMsg(False, '文件不能为空')) |
|
|
| if not os.path.exists(file): |
| return self.return_msg(public.returnMsg(False, '文件不存在')) |
|
|
| try: |
| data = public.readFile(file) |
| data = json.loads(data) |
| except Exception as e: |
| return self.return_msg(public.returnMsg(False, '文件内容异常或格式错误: {}'.format(e))) |
| args.emails_to_add = data |
| self.add_recipient_blacklist(args) |
| return self.return_msg(public.returnMsg(True, '成功导入黑名单')) |
| except: |
| public.print_log(public.get_error_info()) |
|
|
| |
|
|
| |
| def get_abnormal_recipient(self, args): |
|
|
| p = int(args.p) if 'p' in args else 1 |
| rows = int(args.size) if 'size' in args else 12 |
| data_list = [] |
|
|
| if "search" in args and args.search != "": |
| where_str = "recipient LIKE ? OR task_name LIKE ?" |
| where_args = (f"%{args.search.strip()}%", f"%{args.search.strip()}%") |
| else: |
| where_str = "id!=?" |
| where_args = (0,) |
|
|
| if 'status' in args and args.status != "": |
| status = args.status |
| if where_str and where_args: |
| where_str = "status=? AND (recipient LIKE? OR task_name LIKE ?)" |
|
|
| where_args = (status, f"%{args.search.strip()}%", f"%{args.search.strip()}%") |
| else: |
| where_str = "status=?" |
| where_args = (status,) |
|
|
| with public.S("abnormal_recipient", "/www/vmail/abnormal_recipient") as obj: |
| count = obj.where(where_str, where_args).select() |
| data_list = obj.order('created', 'DESC').limit(rows, (p - 1) * rows).where(where_str, |
| where_args).select() |
|
|
| for i in data_list: |
| if i['status'] == 'bounced': |
| i['state'] = 1 |
| else: |
| i['state'] = 1 if i['count'] >= 3 else 0 |
|
|
| return self.return_msg({'data': data_list, 'total': len(count)}) |
|
|
| else: |
| with public.S("abnormal_recipient", "/www/vmail/abnormal_recipient") as obj: |
| count = obj.where(where_str, where_args).select() |
| data_list = obj.order('created', 'DESC').limit(rows, (p - 1) * rows).where(where_str, |
| where_args).select() |
|
|
| for i in data_list: |
| if i['status'] == 'bounced': |
| i['state'] = 1 |
| else: |
| i['state'] = 1 if i['count'] >= 3 else 0 |
| |
| return self.return_msg({'data': data_list, 'total': len(count)}) |
|
|
| def get_abnormal_status(self, args): |
| with public.S("abnormal_recipient", "/www/vmail/abnormal_recipient") as obj: |
| count = obj.group('status').field('status').select() |
| return self.return_msg(count) |
|
|
| |
| def del_abnormal_recipient(self, args): |
| try: |
| |
| if "ids" in args and args.ids != "": |
| ids_list = args.ids.split(',') |
| ids_list = [int(id_str) for id_str in ids_list] |
| with public.S("abnormal_recipient", "/www/vmail/abnormal_recipient.db") as obj: |
| nums = obj.where_in('id', ids_list).column('id') |
| if len(nums) > 0: |
| obj.where_in('id', ids_list).delete() |
| return self.return_msg(public.returnMsg(True, public.lang('删除成功'))) |
| except Exception as e: |
| return self.return_msg(public.returnMsg(False, public.lang('错误: {}', e))) |
|
|
| |
| def clear_abnormal_recipient(self, args): |
| try: |
|
|
| if "status" in args and args.status != "": |
| status = args.status |
| else: |
| status = 'all' |
|
|
| if status == 'all': |
| with public.S("abnormal_recipient", "/www/vmail/abnormal_recipient.db") as obj: |
| obj.delete() |
| else: |
| with public.S("abnormal_recipient", "/www/vmail/abnormal_recipient.db") as obj: |
| nums = obj.where('status', status).delete() |
| public.print_log('清空 {}个'.format(nums)) |
| return self.return_msg(public.returnMsg(True, public.lang('清空 {} 成功', status))) |
| except Exception as e: |
| return self.return_msg(public.returnMsg(False, public.lang('错误: {}', e))) |
|
|
| def _sync_blacklist_to_unsubscribe_db(self): |
| |
| |
| if not os.path.exists('/www/vmail'): |
| return |
| |
| path = '/www/server/panel/data/mail_sync_black_to_unsubscribe_db.pl' |
| if os.path.exists(path): |
| return |
|
|
| recipient_blacklist = [] |
| if not self._recipient_blacklist_status() or not os.path.exists(self.postfix_recipient_blacklist): |
| |
| recipient_blacklist = [] |
| else: |
|
|
| try: |
| with open(self.postfix_recipient_blacklist, 'r') as file: |
| emails = file.read().splitlines() |
| except Exception as e: |
| emails = [] |
| |
| if emails: |
| recipient_blacklist = [email.split()[0] for email in emails] |
|
|
| |
| if recipient_blacklist: |
| created = int(time.time()) |
| insert_data = [] |
| for recipient in recipient_blacklist: |
| insert_data.append({ |
| "created": created, |
| "recipient": recipient, |
| "etype": 0, |
| }) |
|
|
| |
| with public.S("mail_unsubscribe", "/www/vmail/mail_unsubscribe.db") as obj: |
| aa = obj.insert_all(insert_data, option='IGNORE') |
| |
| |
| |
|
|
| |
| st = self.recipient_blacklist_open(False) |
| if st: |
| public.ExecShell('systemctl reload postfix') |
|
|
| |
| public.writeFile(path, 1) |
|
|
| |
| def get_unsubscribe_list(self, args): |
| ''' |
| 获取退订用户列表 |
| :param args: etype 邮件类型id |
| :param args: search 搜索 收件人 |
| :param args: active 类型 0退订 1订阅 |
| :return: |
| ''' |
| p = int(args.p) if 'p' in args else 1 |
| rows = int(args.size) if 'size' in args else 12 |
| active = args.active if 'active' in args else 0 |
|
|
| if "search" in args and args.search != "": |
| where_str = "recipient LIKE ? AND active=?" |
| where_args = (f"%{args.search.strip()}%", active) |
| else: |
| |
| where_str = "active=?" |
| where_args = (active,) |
|
|
| |
| typelist = self.get_mail_type_list(None) |
| typelist = {str(item["id"]): item["mail_type"] for item in typelist} |
|
|
| if 'etype' in args and args.etype != "": |
| etype = int(args.etype) |
| if where_str and where_args: |
| where_str = "etype=? AND recipient LIKE? AND active=?" |
| where_args = (etype, f"%{args.search.strip()}%", active) |
| else: |
| where_str = "etype=? AND active=?" |
| where_args = (etype, active) |
|
|
| with public.S("mail_unsubscribe", '/www/vmail/mail_unsubscribe.db') as obj: |
| query = obj.where(where_str, where_args).group('recipient') |
| count = query.select() |
| data_list = obj.order('created', 'DESC').limit(rows, (p - 1) * rows).where(where_str, where_args).group( |
| 'recipient').select() |
| for i in data_list: |
| i['mail_type'] = [] |
| etypes = obj.where('active', active).where('recipient', i['recipient']).field('etype').select() |
| |
| for j in etypes: |
| if typelist.get(str(j['etype']), None): |
| i['mail_type'].append({str(j['etype']): typelist[str(str(j['etype']))]}) |
| else: |
| ... |
|
|
| return self.return_msg({'data': data_list, 'total': len(count)}) |
|
|
| else: |
| with public.S("mail_unsubscribe", '/www/vmail/mail_unsubscribe.db') as obj: |
| count = obj.where(where_str, where_args).group('recipient').select() |
|
|
| data_list = obj.order('created', 'DESC').limit(rows, (p - 1) * rows).where(where_str, where_args).group( |
| 'recipient').select() |
| for i in data_list: |
| i['mail_type'] = [] |
| etypes = obj.where('active', active).where('recipient', i['recipient']).field('etype').select() |
| |
| for j in etypes: |
| if typelist.get(str(j['etype']), None): |
| i['mail_type'].append({str(j['etype']): typelist[str(str(j['etype']))]}) |
| else: |
| ... |
| return self.return_msg({'data': data_list, 'total': len(count)}) |
|
|
| def get_contacts_list(self, args): |
| ''' |
| 获取联系人列表 趋势图展示 |
| :param args: active 类型 0退订 1订阅 |
| :return: |
| ''' |
|
|
| from datetime import datetime |
| from collections import defaultdict |
|
|
| active = args.active if 'active' in args else 0 |
|
|
| with public.S("mail_unsubscribe", '/www/vmail/mail_unsubscribe.db') as obj: |
| query = obj.where('active', active).order('created', 'DESC').select() |
|
|
| |
| monthly_data = defaultdict(lambda: {'count': 0, 'month': ''}) |
|
|
| |
| for record in query: |
| created_timestamp = record['created'] |
| created_date = datetime.utcfromtimestamp(created_timestamp) |
| year_month = created_date.strftime('%Y-%m') |
| monthly_data[year_month]['count'] += 1 |
| monthly_data[year_month]['month'] = year_month |
|
|
| |
| if monthly_data: |
| last_month = max(monthly_data.keys()) |
| last_month_date = datetime.strptime(last_month, '%Y-%m') |
| |
| result = self.complete_monthly_data(list(monthly_data.values()), last_month_date) |
|
|
| return self.return_msg(result) |
| else: |
| return self.return_msg([]) |
|
|
| def complete_monthly_data(self, data, last_month_date): |
| """ 补全12个月数据 """ |
| from datetime import datetime |
| from collections import defaultdict |
| from dateutil.relativedelta import relativedelta |
| |
| months_list = [] |
| for i in range(12): |
| |
| month_date = last_month_date - relativedelta(months=i) |
| months_list.append(month_date.strftime('%Y-%m')) |
|
|
| |
| data_dict = {entry['month']: entry['count'] for entry in data} |
|
|
| |
| completed_data = [] |
| for month in months_list[::-1]: |
| if month in data_dict: |
| completed_data.append({'month': month, 'count': data_dict[month]}) |
| else: |
| completed_data.append({'month': month, 'count': 0}) |
|
|
| return completed_data |
|
|
| def edit_type_unsubscribe_list(self, args): |
| """切换联系人的列表类型""" |
| etypes_list = [] |
| recipients_list = [] |
|
|
| if "active" not in args or args.active == "": |
| return self.return_msg(public.returnMsg(False, public.lang('缺少参数: active'))) |
| active = int(args.active) |
|
|
| |
| if "etypes" in args and args.etypes != "": |
| etypes_list = args.etypes.split(',') |
|
|
| |
| if "recipients" in args and args.recipients != "": |
| recipients_list = args.recipients.split(',') |
| created = int(time.time()) |
| try: |
| insert_data_alletype = [] |
| with public.S("mail_unsubscribe", "/www/vmail/mail_unsubscribe.db") as obj: |
| |
| aa = obj.where('active', active).where_in('recipient', recipients_list).delete() |
|
|
| for etype in etypes_list: |
| insert_data = [] |
| for recipients in recipients_list: |
| insert_data.append({ |
| 'created': created, |
| 'recipient': recipients, |
| 'etype': int(etype), |
| 'active': active, |
| }) |
|
|
| insert_data_alletype += insert_data |
|
|
| num = obj.insert_all(insert_data_alletype, option='IGNORE') |
|
|
| except Exception as e: |
| return self.return_msg(public.returnMsg(False, public.lang('错误: {}', e))) |
| return self.return_msg(public.returnMsg(True, public.lang('类型修改成功'))) |
|
|
| def update_subscription_state(self, args): |
| """切换订阅退订状态""" |
| try: |
| if "active" not in args or args.active == "": |
| return public.returnMsg(False, public.lang('Missing parameter: active')) |
| if "recipient" not in args or args.recipient == "": |
| return public.returnMsg(False, public.lang('Missing parameter: recipient')) |
| active = int(args.active) |
| recipient = args.recipient |
| with public.S("mail_unsubscribe", "/www/vmail/mail_unsubscribe.db") as obj: |
| obj.where('recipient', recipient).update({'active': active}) |
| except Exception as e: |
| return self.return_msg(public.returnMsg(False, public.lang('错误: {}', e))) |
| return self.return_msg(public.returnMsg(True, public.lang('修改成功'))) |
|
|
| |
| def del_unsubscribe_list(self, args): |
| try: |
| if "active" not in args or args.active == "": |
| return self.return_msg(public.returnMsg(False, public.lang('缺少参数: active'))) |
| active = int(args.active) |
| if "ids" in args and args.ids != "": |
| ids_list = args.ids.split(',') |
| ids_list = [int(id_str) for id_str in ids_list] |
| with public.S("mail_unsubscribe", "/www/vmail/mail_unsubscribe.db") as obj: |
| nums = obj.where('active', active).where_in('id', ids_list).column('id') |
| if len(nums) > 0: |
| delnum = obj.where('active', active).where_in('id', ids_list).delete() |
| except Exception as e: |
| return self.return_msg(public.returnMsg(False, public.lang('错误: {}', e))) |
| return self.return_msg(public.returnMsg(True, public.lang('删除成功'))) |
|
|
| |
| def add_unsubscribe1(self, args): |
| |
| email = args.email |
| etype = int(args.etype) |
|
|
| try: |
|
|
| for i in range(9): |
| created = int(time.time()) |
| email = f"tet{i}@qq.cpm" |
| insert = { |
| 'created': created, |
| 'recipient': email, |
| 'etype': 8, |
| } |
|
|
| with self.MD("mail_unsubscribe", "mail_unsubscribe") as obj: |
| obj.insert(insert) |
| return self.return_msg(True) |
| except Exception as e: |
| return self.return_msg(False) |
|
|
| |
| def add_unsubscribe(self, args): |
| |
| email = args.emails |
| etype = int(args.etype) |
| emaillist = email.splitlines() |
| active = int(args.active) |
|
|
| try: |
| insert_data = [] |
| for i in emaillist: |
| created = int(time.time()) |
| email = i |
| insert_data.append({ |
| 'created': created, |
| 'recipient': email, |
| 'etype': etype, |
| 'active': active, |
| }) |
| with public.S("mail_unsubscribe", '/www/vmail/mail_unsubscribe.db') as obj: |
| num = obj.insert_all(insert_data, option='IGNORE') |
| |
| return self.return_msg(public.returnMsg(True, public.lang('添加成功', num))) |
|
|
| except Exception as e: |
| return self.return_msg(public.returnMsg(False, public.lang('添加失败 {}', e))) |
|
|
| def get_mail_type_list(self, args): |
| ''' |
| 获取邮件类型列表 |
| :param args: |
| :return: |
| ''' |
|
|
| |
| with self.M('mail_type') as obj: |
| data_list = obj.order('created desc').select() |
|
|
| return data_list |
|
|
| def get_mail_type_info_list(self, args): |
| ''' |
| 获取邮件类型列表 |
| :param args: search 搜索 |
| :return: |
| ''' |
|
|
| if "search" in args and args.search != "": |
| where_str = "mail_type LIKE ?" |
| where_args = (f"%{args.search.strip()}%") |
| else: |
| |
| where_str = "id!=?" |
| where_args = (0,) |
| p = int(args.p) if 'p' in args else 1 |
| rows = int(args.size) if 'size' in args else 10 |
| |
| with self.M('mail_type') as obj: |
| count = obj.order('created desc').where(where_str, where_args).count() |
| data_list = obj.order('created desc').where(where_str, where_args).limit(rows, (p - 1) * rows).select() |
|
|
| with public.S("mail_unsubscribe", '/www/vmail/mail_unsubscribe.db') as obj: |
| etypes = obj.where('active', 1).group('etype').field('etype', 'count(*) as `cnt`').select() |
| unetypes = obj.where('active', 0).group('etype').field('etype', 'count(*) as `cnt`').select() |
| |
| etype_cnt = {} |
| unetype_cnt = {} |
| for i in etypes: |
| etype_cnt[str(i['etype'])] = i['cnt'] |
| for i in unetypes: |
| unetype_cnt[str(i['etype'])] = i['cnt'] |
| |
| |
| |
| from mailModel.bulkModel import main as SendMailBulk |
|
|
| url = SendMailBulk().get_unsubscribe_url() |
|
|
| for i in data_list: |
| i['subscribe_url'] = f"{url}/mailUnsubscribe?action=Subscribe&etype={i['id']}" |
| i['subscribers'] = etype_cnt.get(str(i['id']), 0) |
| i['unsubscribers'] = unetype_cnt.get(str(i['id']), 0) |
|
|
| return self.return_msg({'data': data_list, 'total': count}) |
|
|
| |
| def del_mail_type_list(self, args): |
| |
| delnum = 0 |
| ids_err = 0 |
| if "ids" in args and args.ids != "": |
| ids_list = args.ids.split(',') |
| ids_list = [int(id_str) for id_str in ids_list if id_str != '1'] |
| ids_ok = [] |
| with self.MD("mail_unsubscribe", "mail_unsubscribe") as obj: |
| for etype_id in ids_list: |
| count = obj.where('etype=?', etype_id).count() |
| if count > 0: |
| ids_err += 1 |
| else: |
| ids_ok.append(etype_id) |
|
|
| with public.S("mail_type", "/www/vmail/postfixadmin.db") as obj: |
| nums = obj.where_in('id', ids_ok).column('id') |
| if len(nums) > 0: |
| delnum = obj.where_in('id', ids_ok).delete() |
| if delnum == 0 and ids_err == 0: |
| return self.return_msg(public.returnMsg(False, public.lang('默认类型不能删除'))) |
| if ids_err > 0: |
| return self.return_msg(public.returnMsg(True, |
| public.lang('成功删除{}种类型,有{}种类型正在使用不能删除', |
| delnum, ids_err))) |
| return self.return_msg(public.returnMsg(True, public.lang('删除成功'))) |
|
|
| |
| def edit_mail_type(self, args): |
| id = int(args.id) |
| mail_type = args.mail_type |
|
|
| if id == 1: |
| return self.return_msg(public.returnMsg(False, public.lang('默认类型不能修改'))) |
|
|
| try: |
| with self.M('mail_type') as obj: |
| info = obj.where('id=?', id).update({"mail_type": mail_type}) |
| |
| return self.return_msg(public.returnMsg(True, public.lang('修改成功'))) |
| except Exception as e: |
| return self.return_msg(public.returnMsg(False, public.lang('错误: {}', e))) |
|
|
| |
| def add_mail_type(self, args): |
| mail_type = args.mail_type |
| try: |
| created = int(time.time()) |
| insert = { |
| 'created': created, |
| 'mail_type': mail_type, |
| } |
|
|
| with self.M('mail_type') as obj: |
| exit = obj.where('mail_type =?', (mail_type,)).count() |
| if exit: |
| return self.return_msg(public.returnMsg(False, public.lang('类型已存在'))) |
| obj.insert(insert) |
| return self.return_msg(public.returnMsg(True, public.lang('添加成功'))) |
| except Exception as e: |
| return self.return_msg(public.returnMsg(False, public.lang('添加失败 {}', e))) |
|
|
| |
| def get_mail_type(self, args): |
| id = args.id |
| try: |
| with self.M('mail_type') as obj: |
| info = obj.where('id=?', id).find() |
| return self.return_msg(public.returnMsg(True, info)) |
| except Exception as e: |
| return self.return_msg(public.returnMsg(False, public.lang('错误: {}', e))) |
|
|
| |
| def export_unsubscribe_list(self, args): |
| |
| ... |
|
|
| |
| def import_unsubscribe_list(self, args): |
| ... |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| def tables2(self, get): |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| ... |
|
|
| def get_task_list(self, args): |
| ''' |
| 任务列表 |
| :param args: |
| :return: |
| ''' |
| |
| |
| import PluginLoader |
| args.model_index = 'mail' |
| try: |
| res = PluginLoader.module_run("bulk", "get_task_list", args) |
| return self.return_msg(res) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return self.return_msg({}) |
|
|
| |
| def check_task_status(self, args): |
| ''' |
| 执行发送邮件的定时任务 |
| :param |
| :return: |
| ''' |
|
|
| import PluginLoader |
| args.model_index = 'mail' |
| |
| service_status = self.get_service_status(None)['data'] |
| if not service_status['postfix']: |
| return self.return_msg(False) |
| |
| if not self._check_smtp_port(): |
| return self.return_msg(False) |
|
|
| try: |
| res = PluginLoader.module_run("bulk", "check_task_status", args) |
| return self.return_msg(res) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return self.return_msg({}) |
|
|
| |
| def check_task_finish(self, args): |
| ''' |
| 发送完毕后处理发送失败的日志 |
| :param |
| :return: |
| ''' |
|
|
| import PluginLoader |
| args.model_index = 'mail' |
| try: |
| res = PluginLoader.module_run("bulk", "check_task_finish", args) |
| return self.return_msg(res) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return self.return_msg({}) |
|
|
| def processing_recipient(self, args): |
| ''' |
| 导入收件人 |
| :param file |
| :return: |
| ''' |
|
|
| import PluginLoader |
| args.model_index = 'mail' |
|
|
| try: |
| res = PluginLoader.module_run("bulk", "processing_recipient", args) |
| return self.return_msg(res) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return self.return_msg({}) |
|
|
| def get_recipient_data(self, args): |
| ''' |
| 获取发送预计完成时间 |
| :param file |
| :return: |
| ''' |
|
|
| import PluginLoader |
| args.model_index = 'mail' |
| try: |
| res = PluginLoader.module_run("bulk", "get_recipient_data", args) |
| return self.return_msg(res) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return self.return_msg({}) |
|
|
| def add_task(self, args): |
| ''' |
| 添加批量发送任务 |
| :param args: |
| :return: |
| ''' |
| |
| |
| import PluginLoader |
| args.model_index = 'mail' |
| try: |
| res = PluginLoader.module_run("bulk", "add_task", args) |
| return self.return_msg(res) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return self.return_msg({}) |
|
|
| def pause_task(self, args): |
| ''' |
| 暂停发送任务 判断状态为执行中的可以暂停 task_process 1 |
| :param args: task_id 任务id; pause 1暂停 0 重启 |
| :return: |
| ''' |
|
|
| import PluginLoader |
| args.model_index = 'mail' |
| try: |
| res = PluginLoader.module_run("bulk", "pause_task", args) |
| return self.return_msg(res) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return self.return_msg({}) |
|
|
| def delete_task(self, args): |
| ''' |
| 删除任务 |
| :param args: task_id 任务id |
| :return: |
| ''' |
|
|
| import PluginLoader |
| args.model_index = 'mail' |
| try: |
| res = PluginLoader.module_run("bulk", "delete_task", args) |
| return self.return_msg(res) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return self.return_msg({}) |
|
|
| def get_log_rank(self, args): |
| ''' |
| 获取错误排行 |
| :param args: task_id 任务id |
| :return: |
| ''' |
|
|
| import PluginLoader |
| args.model_index = 'mail' |
| try: |
| res = PluginLoader.module_run("bulk", "get_log_rank", args) |
| return self.return_msg(res) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return self.return_msg({}) |
|
|
| def get_log_list(self, args): |
| ''' |
| 获取错误详情 |
| :param args: task_id 任务id |
| :return: |
| ''' |
| import PluginLoader |
| args.model_index = 'mail' |
| try: |
| res = PluginLoader.module_run("bulk", "get_log_list", args) |
| return self.return_msg(res) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return self.return_msg({}) |
|
|
| |
| def get_task_email_content(self, args): |
|
|
| if 'id' in args and args.id != '': |
| id = int(args.id) |
| else: |
| return public.returnMsg(False, public.lang('id必传')) |
|
|
| email_info = self.M('temp_email').where('id=?', id).find() |
| if not email_info: |
| return public.returnMsg(False, public.lang('模板不存在')) |
|
|
| content_path = email_info['content'] |
| render_path = email_info['render'] |
| type = email_info['type'] |
| if os.path.exists(content_path): |
| content = public.readFile(content_path) |
| |
| |
| |
| |
| else: |
| content = '{}文件不存在'.format(content_path) |
|
|
| if type: |
| if os.path.exists(render_path): |
| render = public.readFile(render_path) |
| |
| |
| |
| |
| else: |
| render = '{}文件不存在'.format(render_path) |
| else: |
| render = '' |
|
|
| data = { |
| 'name': email_info['name'], |
| 'type': email_info['type'], |
| 'content_path': content_path, |
| 'content': content, |
| 'render_path': render_path, |
| 'render': render, |
| } |
| return self.return_msg(data) |
|
|
| |
| def get_task_find(self, args): |
|
|
| if 'id' in args and args.id != '': |
| id = int(args.id) |
| else: |
| return public.returnMsg(False, public.lang('id必传')) |
| |
| task_info = self.M('email_task').where('id=?', id).find() |
| if not isinstance(task_info, dict): |
| return public.returnMsg(False, task_info) |
| email_info = self.M('temp_email').where('id=?', task_info['temp_id']).find() |
|
|
| data = { |
| "task_info": task_info, |
| "email_info": email_info, |
| } |
|
|
| return self.return_msg(data) |
|
|
| def update_task(self, args): |
| ''' |
| 修改发送任务 |
| :param args: |
| :return: |
| ''' |
| |
| |
| import public.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
| try: |
| return SendMailBulk().update_task(args) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return {} |
|
|
| |
| def get_data_info(self, args): |
| now = datetime.now() |
|
|
| |
| today_start = datetime(now.year, now.month, now.day) |
|
|
| |
| start = int(today_start.timestamp()) |
| end = start + 86400 |
| |
| cache_key = 'mail_sys:get_day_errlog_{}'.format(start) |
| cache = public.cache_get(cache_key) |
| if cache: |
| return self.return_msg(cache) |
| try: |
|
|
| with self.MD("mail_errlog", "postfixmaillog") as obj2: |
| |
| query = obj2.order('created desc').where('created >? AND created<?', (start, end)).select() |
|
|
|
|
| except: |
| public.print_log(public.get_error_info()) |
|
|
| |
| public.cache_set(cache_key, query, 30 * 60) |
|
|
| return self.return_msg(query) |
|
|
| |
| def _get_errlist(self, timestamp): |
| ''' |
| 获取错误详情 |
| :return: |
| ''' |
|
|
| start = int(timestamp) |
| end = int(timestamp) + 3599 |
| |
| current_time = int(time.time()) |
| if current_time < start: |
| return [] |
|
|
| |
| cache_key = 'mail_sys:get_errlog_{}'.format(timestamp) |
| cache = public.cache_get(cache_key) |
| if cache: |
| return cache |
| try: |
| with self.MD("mail_errlog", "postfixmaillog") as obj2: |
| query = obj2.order('created desc').where('created >? AND created<?', (start, end)).select() |
| except: |
| public.print_log(public.get_error_info()) |
|
|
| |
| if current_time > end: |
| public.cache_set(cache_key, query, 60 * 60 * 24) |
| else: |
| public.cache_set(cache_key, query, 30 * 60) |
|
|
| return query |
|
|
| def task_cut_maillog(self): |
| cmd = ''' |
| if pgrep -f "cut_maillog.py" > /dev/null |
| then |
| echo "The task [Cut_maillog] is executing" |
| exit 1; |
| else |
| btpython /www/server/panel/class/mailModel/script/cut_maillog.py |
| fi |
| ''' |
|
|
| import crontab |
| p = crontab.crontab() |
| try: |
| c_id = public.M('crontab').where('name=?', u'[勿删] 切割邮件日志').getField('id') |
| if not c_id: |
| data = {} |
| data['name'] = u'[勿删] 切割邮件日志' |
| data['type'] = 'hour-n' |
| data['where1'] = '1' |
| data['sBody'] = 'btpython /www/server/panel/class/mailModel/script/cut_maillog.py' |
| data['backupTo'] = '' |
| data['sType'] = 'toShell' |
| data['hour'] = '' |
| data['minute'] = '0' |
| data['week'] = '' |
| data['sName'] = '' |
| data['urladdress'] = '' |
| data['save'] = '' |
| p.AddCrontab(data) |
| return public.returnMsg(True, '设置成功!') |
| |
| |
| |
| |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
|
|
| |
| def set_unsubscribe_info(self, args): |
| path_info = {} |
| if os.path.exists(self.unsubscribe_path): |
| path_info = json.loads(public.readFile(self.unsubscribe_path)) |
|
|
| if 'url' in args and args.url != '': |
| |
| url = args.url |
| td = "{}/mailUnsubscribe".format(url) |
|
|
| try: |
| testdata = public.httpGet(td) |
| public.print_log(testdata) |
| try: |
| testdata = json.loads(testdata) |
| except: |
| pass |
| if isinstance(testdata, dict): |
| |
| |
|
|
| if testdata['status'] == 0: |
|
|
| path_info['url'] = url |
| else: |
| return self.return_msg(public.returnMsg(False, public.lang( |
| '当前url无法访问,请正确设置反向代理!'))) |
| else: |
| return self.return_msg(public.returnMsg(False, public.lang( |
| '当前url无法访问,请正确设置反向代理!!'))) |
| except Exception as e: |
| return self.return_msg(public.returnMsg(False, e)) |
| public.writeFile(self.unsubscribe_path, json.dumps(path_info)) |
| |
|
|
| return self.return_msg(public.returnMsg(True, public.lang('设置成功!'))) |
|
|
| |
| def get_unsubscribe_info(self, args): |
| |
| ssl_staus = public.readFile('/www/server/panel/data/ssl.pl') |
| if ssl_staus: |
| ssl = 'https' |
| else: |
| ssl = 'http' |
|
|
| ip = public.readFile("/www/server/panel/data/iplist.txt") |
| port = public.readFile('/www/server/panel/data/port.pl') |
| panel_url = "{}://{}:{}".format(ssl, ip, port) |
|
|
| if os.path.exists(self.unsubscribe_path): |
| path_info = json.loads(public.readFile(self.unsubscribe_path)) |
| url = path_info.get('url', '') |
| else: |
| url = '' |
|
|
| data = { |
| "url": url, |
| "panel_url": panel_url |
| } |
|
|
| return self.return_msg(public.returnMsg(True, data)) |
|
|
| |
| def del_unsubscribe_info(self, args): |
| if os.path.exists(self.unsubscribe_path): |
| os.remove(self.unsubscribe_path) |
|
|
| return self.return_msg(public.returnMsg(True, public.lang('删除成功'))) |
|
|
| def __check_auth(self): |
| |
| from plugin_auth_v2 import Plugin as Plugin |
| plugin_obj = Plugin(False) |
| plugin_list = plugin_obj.get_plugin_list() |
| |
| import PluginLoader |
| self.__IS_PRO_MEMBER = PluginLoader.get_auth_state() > 0 |
| return int(plugin_list["pro"]) > time.time() or self.__IS_PRO_MEMBER |
|
|
| def modify_domain_quota(self, args): |
| if not hasattr(args, "path"): |
| return self.return_msg(public.returnMsg(False, "缺少参数!path")) |
| if not hasattr(args, "quota_type"): |
| return self.return_msg(public.returnMsg(False, "缺少参数!quota_type")) |
| if not hasattr(args, "quota_storage"): |
| return self.return_msg(public.returnMsg(False, "缺少参数!quota_storage")) |
|
|
| quota_type = args.quota_type |
| if not isinstance(args.quota_storage, dict): |
| return self.return_msg(public.returnMsg(False, "参数错误! quota_storage")) |
| path = args.path |
| path = str(path).rstrip("/") |
| if not os.path.exists(path): |
| return self.return_msg(public.returnMsg(False, "指定目录不存在")) |
| if os.path.isfile(path): |
| return self.return_msg(public.returnMsg(False, "这不是一个有效的目录!")) |
| if os.path.islink(path): |
| return self.return_msg(public.return_message(False, "指定的目录是软链接!")) |
| if not os.path.isdir(path): |
| return self.return_msg(public.returnMsg(False, "这不是一个有效的目录!")) |
|
|
| return self.return_msg(self.modify_path_quota(args)) |
|
|
| ... |
|
|
| def modify_path_quota(self, args): |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| path = args.path |
| path = str(path).rstrip("/") |
| quota_type = args.quota_type |
| quota_dict = self.__get_quota_list() |
|
|
| if quota_dict.get(path) is not None: |
| |
| |
| |
| quota = quota_dict[path] |
| quota["quota_push"]["size"] = int(args.quota_push.get("size", 0)) |
| quota["quota_push"]["interval"] = int( |
| args.quota_push.get("interval", 600)) |
| quota["quota_push"]["module"] = args.quota_push["module"] |
| quota["quota_push"]["push_count"] = int( |
| args.quota_push.get("push_count", 3)) |
| quota["quota_push"]["status"] = args.quota_push.get( |
| "status", False) |
| quota["quota_storage"]["size"] = int( |
| args.quota_storage.get("size", 0)) |
| else: |
| quota = { |
| "id": self.__get_quota_id(quota_dict), |
| "quota_type": quota_type, |
| "quota_push": { |
| "size": int(args.quota_push.get("size", 0)), |
| "interval": int(args.quota_push.get("interval", 600)), |
| "module": args.quota_push.get("module", ""), |
| "push_count": int(args.quota_push.get("push_count", 3)), |
| "status": args.quota_push.get("status", False), |
| }, |
| "quota_storage": { |
| "size": int(args.quota_storage.get("size", 0)), |
| }, |
| } |
|
|
| if quota["quota_storage"]["size"] > 0: |
| disk = self.__get_path_dev_mountpoint(path) |
| if disk is None: |
| return self.return_msg(public.returnMsg( |
| False, |
| "指定目录所在的分区不是 XFS 分区,且不支持目录配额!" |
| )) |
|
|
| if "prjquota" not in disk["opts"]: |
| msg = '<div class="ftp-verify-disk">指定的 xfs 分区未启用目录配额。请在挂载该分区时添加 `prjquota` 参数。 <p>/etc/fstab 示例文件配置:</p><pre>{device} {mountpoint} xfs defaults,prjquota 0 0</pre><p>注意:配置后,需要重新挂载分区或重启服务器以使配置生效。</p></div>'.format( |
| device=disk["device"], mountpoint=disk["mountpoint"]) |
| return self.return_msg(public.returnMsg(False, msg)) |
|
|
| if args.quota_storage.get("size", 0) * 1024 * 1024 > disk["free"]: |
| return self.return_msg(public.returnMsg( |
| False, |
| "指定磁盘的可用配额容量不足!" |
| )) |
|
|
| res = public.ExecShell( |
| "xfs_quota -x -c 'project -s -p {path} {quota_id}'".format( |
| path=path, quota_id=quota["id"])) |
| if res[1]: |
| return self.return_msg(public.returnMsg( |
| False, "配额设置错误!{}".format(res[1]))) |
| res = public.ExecShell( |
| "xfs_quota -x -c 'limit -p bhard={size}m {quota_id}' {mountpoint}" |
| .format(size=quota["quota_storage"]["size"], |
| quota_id=quota["id"], |
| mountpoint=disk["mountpoint"])) |
| if res[1]: |
| return self.return_msg(public.returnMsg( |
| False, "配额设置错误!{}".format(res[1]))) |
|
|
| self.__set_push(quota) |
|
|
| quota_dict[path] = quota |
| public.WriteLog( |
| "配额", |
| "设置目录[{path}]的配额限制为: {size}MB".format( |
| path=path, size=quota["quota_storage"]["size"])) |
| public.writeFile(self.__SETTINGS_FILE, json.dumps(quota_dict)) |
| return self.return_msg(public.returnMsg(True, "修改成功")) |
|
|
| |
| def remove_old_cron(self): |
| |
| if not os.path.exists('/www/vmail'): |
| return |
| |
| path = '/www/server/panel/data/remove_old_mail_cron.pl' |
| if os.path.exists(path): |
| return |
| |
| c_id = public.M('crontab').where('name=?', u'[勿删] 切割邮件日志').getField('id') |
| if not c_id: |
| return |
| target_list = ['cut_maillog.py', 'send_bulk_script.py', 'mail_error_logs.py'] |
| cron_jobs = public.ExecShell("crontab -l") |
| |
| script_path_list = [] |
| if cron_jobs: |
| |
| script_paths = self.find_script_paths(cron_jobs) |
|
|
| |
| for script_path in script_paths: |
| if self.search_in_file(script_path, target_list): |
| script_path_list.append(script_path) |
|
|
| import crontab |
| p = crontab.crontab() |
| try: |
| |
| for echo_path in script_path_list: |
| echo = echo_path.split('/')[-1] |
|
|
| p.remove_for_crond(echo) |
| if os.path.exists(echo_path): os.remove(echo_path) |
| sfile = echo_path + '.log' |
| if os.path.exists(sfile): os.remove(sfile) |
| except: |
| pass |
| try: |
| |
| c_id = public.M('crontab').where('name=?', u'[勿删] 检查发送结果').getField('id') |
| if c_id: |
| a = p.DelCrontab({"id": c_id}) |
|
|
| s_id = public.M('crontab').where('name=?', u'[勿删] 群发邮件任务').getField('id') |
| if s_id: |
| b = p.DelCrontab({"id": s_id}) |
| |
| m_id = public.M('crontab').where('name=?', u'[勿删] 切割邮件日志').getField('id') |
| if m_id: |
| c = p.DelCrontab({"id": m_id}) |
| |
| except: |
| public.print_log(public.get_error_info()) |
|
|
| |
| public.writeFile(path, "") |
| return |
|
|
| def search_in_file(self, file_path, target_list): |
| """检查脚本文件中是否包含目标字符串""" |
| if not os.path.exists(file_path): |
| return False |
| if not os.path.isfile(file_path): |
| return False |
| with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: |
| content = f.read() |
| for target in target_list: |
| if target in content: |
| return True |
| return False |
|
|
| def find_script_paths(self, cron_jobs): |
| """从cron任务中提取出所有的脚本路径""" |
| script_paths = [] |
| lines = cron_jobs[0].split("\n") |
|
|
| path_pattern = re.compile(r'(/\S+)(?=\s*(?:>>|\s*$))') |
| for line in lines: |
| if not line: |
| continue |
| match = path_pattern.search(line) |
| if match: |
| script_paths.append(match.group(1)) |
|
|
| return script_paths |
|
|
| def _get_user_quota(self, ): |
| ''' |
| |
| :return: |
| ''' |
| import public.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
| try: |
| return SendMailBulk()._get_user_quota() |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return {} |
|
|
| def import_contacts(self, args): |
| ''' |
| 导入收件人到联系人列表 |
| :param file str (收件人文件名) |
| :param etypes str (联系人类型 多个逗号隔开) 多选分类 每个分类都导入 |
| :param active int (0 退订 1订阅) 暂不使用,默认订阅类型 |
| :return: |
| ''' |
| import public.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
| try: |
| return SendMailBulk().import_contacts(args) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return {} |
|
|
| def import_contacts_etypes(self, args): |
| ''' |
| 导入收件人到联系人列表 |
| :param file str (收件人文件名) |
| :param etypes str (联系人类型 多个逗号隔开) 多选分类 每个分类都导入 |
| :param active int (0 退订 1订阅) 暂不使用,默认订阅类型 |
| :return: |
| ''' |
| import public.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
| try: |
| return SendMailBulk().import_contacts_etypes(args) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return {} |
|
|
| def get_email_temp_list(self, args): |
| ''' |
| 邮件模版列表 |
| :param |
| :return: |
| ''' |
| import mod.base.public_aap.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
| try: |
| return SendMailBulk().get_email_temp_list(args) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return {} |
|
|
| def get_email_temp_render(self, args): |
| ''' |
| 邮件模版列表 |
| :param |
| :return: |
| ''' |
| import public.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
| try: |
| return SendMailBulk().get_email_temp_render(args) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return {} |
|
|
| def get_email_temp(self, args): |
| ''' |
| 邮件模版列表 |
| :param |
| :return: |
| ''' |
| import public.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
| try: |
| return SendMailBulk().get_email_temp(args) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return {} |
|
|
| def add_email_temp(self, args): |
| ''' |
| 邮件模版列表 |
| :param |
| :return: |
| ''' |
| import public.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
| try: |
| return SendMailBulk().add_email_temp(args) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return {} |
|
|
| def del_email_temp(self, args): |
| ''' |
| 邮件模版列表 |
| :param |
| :return: |
| ''' |
| import public.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
| try: |
| return SendMailBulk().del_email_temp(args) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return {} |
|
|
| def edit_email_temp(self, args): |
| ''' |
| 邮件模版列表 |
| :param |
| :return: |
| ''' |
| import public.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
| try: |
| return SendMailBulk().edit_email_temp(args) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return {} |
|
|
| |
| def check_blacklists(self, args): |
| ''' |
| 邮件模版列表 |
| :param |
| :return: |
| ''' |
| import public.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
| try: |
| return SendMailBulk().check_blacklists(args) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return {} |
|
|
| |
| def Blacklist_tips(self, args): |
| ''' |
| 操作黑名单提示横幅 |
| :param operation (-1 忽略 >0 已处理) |
| :return: |
| ''' |
| path = self.blacklist_tips |
| operation = str(args.operation) |
|
|
| |
| if operation == '0': |
| if os.path.exists(path): |
| |
| os.remove(path) |
| return self.return_msg(public.returnMsg(True, public.lang("操作成功"))) |
|
|
| if operation == '-1': |
| oper = -1 |
| else: |
| oper = int(time.time()) |
| public.writeFile(path, str(oper)) |
| return self.return_msg(public.returnMsg(True, public.lang("操作成功"))) |
|
|
| |
| def get_blacklist_tips(self, args): |
| ''' |
| 获取操作黑名单提示横幅处理状态 -1 忽略 0 未操作 >0 已处理 |
| :param |
| :return: |
| ''' |
| path = self.blacklist_tips |
| if os.path.exists(path): |
| data = public.readFile(path) |
| if not data: |
| data = 0 |
| else: |
| data = 0 |
|
|
| blcheck_count = f'/www/server/panel/plugin/mail_sys/data/blcheck.json' |
|
|
| if os.path.exists(blcheck_count): |
| blcheck_ = public.readFile(blcheck_count) |
| try: |
| blcheck_ = json.loads(blcheck_) |
| except: |
| pass |
| count = sum(info["blacklisted"] for info in blcheck_.values()) |
| else: |
| count = 0 |
|
|
| res = { |
| 'status': data, |
| 'count': count, |
| } |
|
|
| return self.return_msg(res) |
|
|
| def _get_alarm_black_switch(self): |
| ''' |
| 获取自动检测黑名单告警开关 |
| :param |
| :return: |
| ''' |
|
|
| |
| |
| |
| |
| |
| |
| path = self.blacklist_alarm_switch |
| if os.path.exists(path): |
| return False |
| else: |
| return True |
|
|
| def set_alarm_black_switch(self, args): |
| ''' |
| 设置自动检测黑名单告警开关 |
| :param type str 'black' |
| :return: |
| ''' |
| operation = str(args.operation) |
|
|
| |
| |
| |
| |
|
|
| |
| path = self.blacklist_alarm_switch |
| if operation == '1': |
| if os.path.exists(path): |
| os.remove(path) |
| |
| else: |
| public.writeFile(path, '1') |
|
|
| public.set_module_logs('mailModel', 'set_alarm_black_switch', 1) |
| return self.return_msg(public.returnMsg(True, public.lang("操作成功"))) |
|
|
| def get_alarm_send(self, args): |
| ''' |
| 获取服务掉线监控告警任务 |
| :param |
| :return: |
| ''' |
| import public.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
| try: |
| return SendMailBulk().get_alarm_send(args) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return {} |
|
|
| |
| def export_email_template(self, args): |
| ''' |
| 导出邮件模版 |
| :param |
| :return: |
| ''' |
| import public.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
| try: |
| return SendMailBulk().export_email_template(args) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return {} |
|
|
| |
| def import_email_template(self, args): |
| ''' |
| 导入邮件模版 |
| :param |
| :return: |
| ''' |
| import public.PluginLoader as plugin_loader |
| bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(public.get_panel_path())) |
| SendMailBulk = bulk.SendMailBulk |
| try: |
| return SendMailBulk().import_email_template(args) |
| except Exception as ex: |
| public.print_log(public.get_error_info()) |
| return {} |
|
|
| def read_blacklist_scan_log(self, args): |
| ''' |
| 读取黑名单扫描日志 |
| :param |
| :return: |
| ''' |
|
|
| if not os.path.exists(args.path): |
| return public.return_message(-1, 0, public.lang("配置文件不存在")) |
| if os.path.isdir(args.path): |
| return public.return_message(-1, 0, public.lang("验证文件写入失败: {}")) |
|
|
| import files |
| f = files.files() |
| public.set_module_logs('mailModel', 'read_blacklist_scan_log', 1) |
| return self.return_msg(f.GetFileBody(args)) |
|
|
| def get_contact_number(self, args): |
| ''' |
| 获取多个分组下的收件人数量 |
| :param str etypes 分组类型 1,3,4 |
| :return: int 数量 |
| ''' |
| if not hasattr(args, 'etypes') or args.get('etypes', '') == '': |
| return self.return_msg(0) |
| else: |
| etypes = args.etypes |
|
|
| etype_list = etypes.split(',') |
|
|
| with public.S("mail_unsubscribe", '/www/vmail/mail_unsubscribe.db') as obj: |
| email_list = obj.where_in('etype', etype_list).where('active', 1).select() |
| emails = [i['recipient'] for i in email_list] |
| |
| count = len(list(set(emails))) |
|
|
| return self.return_msg(count) |
|
|
| def auto_create_dns_record(self, args): |
| """ |
| 自动创建域名的dns记录 |
| @param args: |
| @return: |
| """ |
| try: |
| |
| |
| |
|
|
| |
| |
| |
| item = self.get_record_in_cache(args.__dict__) |
| from sslModel import dataModel |
| dataModel.main() |
| result = [] |
| if item['mx_status'] != 1: |
| try: |
| dataModel.main().add_dns_value_by_domain(args.domain, args.a_record, "MX") |
| result.append({"name": "MX", "status": 1, "msg": "添加成功"}) |
| except Exception as e: |
| result.append({"name": "MX", "status": 0, "msg": str(e)}) |
| if item['spf_status'] != 1: |
| try: |
| dataModel.main().add_dns_value_by_domain(args.domain, "v=spf1 a mx ~all", "TXT") |
| result.append({"name": "SPF", "status": 1, "msg": "添加成功"}) |
| except Exception as e: |
| result.append({"name": "SPF", "status": 0, "msg": str(e)}) |
| if item['dkim_status'] != 1: |
| try: |
| dataModel.main().add_dns_value_by_domain("default._domainkey."+args.domain, self._get_dkim_value(args.domain).strip(), "TXT") |
| result.append({"name": "DKIM", "status": 1, "msg": "添加成功"}) |
| except Exception as e: |
| result.append({"name": "DKIM", "status": 0, "msg": str(e)}) |
| if item['dmarc_status'] != 1: |
| try: |
| dataModel.main().add_dns_value_by_domain("_dmarc."+args.domain, 'v=DMARC1;p=quarantine;rua=mailto:admin@{0}'.format(args.domain), "TXT") |
| result.append({"name": "DMARC", "status": 1, "msg": "添加成功"}) |
| except Exception as e: |
| result.append({"name": "DMARC", "status": 0, "msg": str(e)}) |
| if item['a_status'] != 1: |
| try: |
| dataModel.main().add_dns_value_by_domain(args.a_record, public.get_server_ip(), "A") |
| result.append({"name": "A", "status": 1, "msg": "添加成功"}) |
| except Exception as e: |
| result.append({"name": "A", "status": 0, "msg": str(e)}) |
| result = {"data": result} |
| return self.return_msg(public.returnMsg(True, result)) |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
| return self.return_msg(public.returnMsg(False, '自动创建域名DNS记录失败! {}'.format(e))) |
|
|
| |
| def open_auto_ssl_task(self, get): |
| import crontab |
| import random |
| p = crontab.crontab() |
|
|
| try: |
|
|
| c_id = public.M('crontab').where('name=?', u'[勿删] 堡塔邮局-证书自动续签').getField('id') |
| if c_id: |
| data = {} |
| data['id'] = c_id |
| data['name'] = u'[勿删] 堡塔邮局-证书自动续签' |
| data['type'] = 'day' |
| data['where1'] = '1' |
| data['sBody'] = '/www/server/panel/pyenv/bin/python3 -u /www/server/panel/class/mailModel/script/auto_renew_letssl.py' |
| data['backupTo'] = '' |
| data['sType'] = 'toShell' |
| data['hour'] = random.randint(0, 23) |
| data['minute'] = random.randint(0, 59) |
| data['week'] = '' |
| data['sName'] = '' |
| data['urladdress'] = '' |
| data['save'] = '' |
| p.modify_crond(data) |
| return self.return_msg(public.returnMsg(True, '编辑成功!')) |
| else: |
| data = {} |
| data['name'] = u'[勿删] 堡塔邮局-证书自动续签' |
| data['type'] = 'day' |
| data['where1'] = '1' |
| data['sBody'] = '/www/server/panel/pyenv/bin/python3 -u /www/server/panel/class/mailModel/script/auto_renew_letssl.py' |
| data['backupTo'] = '' |
| data['sType'] = 'toShell' |
| data['hour'] = random.randint(0, 23) |
| data['minute'] = random.randint(0, 59) |
| data['week'] = '' |
| data['sName'] = '' |
| data['urladdress'] = '' |
| data['save'] = '' |
| p.AddCrontab(data) |
| return self.return_msg(public.returnMsg(True, '设置成功!')) |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
|
|
| |
| def close_auto_ssl_task(self, get): |
| import crontab |
|
|
| p = crontab.crontab() |
| c_id = public.M('crontab').where('name=?', u'[勿删] 堡塔邮局-证书自动续签').getField('id') |
| if not c_id: return self.return_msg(public.returnMsg(False, '定时任务不存在!')) |
| args = {"id": c_id} |
| p.DelCrontab(args) |
| return self.return_msg(public.returnMsg(True, '关闭成功!')) |
|
|
| def get_auto_ssl_task_status(self, get): |
| import crontab |
|
|
| p = crontab.crontab() |
| c_id = public.M('crontab').where('name=?', u'[勿删] 堡塔邮局-证书自动续签').getField('id') |
| if not c_id: return self.return_msg(public.returnMsg(False, '定时任务不存在!')) |
| args = {"id": c_id} |
| status = p.GetCrontab(args) |
| if status: |
| return self.return_msg(public.returnMsg(True, '定时任务已开启!')) |
| else: |
| return self.return_msg(public.returnMsg(False, '定时任务已关闭!')) |
|
|
|
|
| def overview_api(self, get): |
| import importlib |
| module = importlib.import_module("mailModel.power_mta.actions") |
| query = request.args |
| if not query.get("action"): |
| return public.returnMsg(False, "参数错误") |
| action = query['action'] |
| get = public.to_dict_obj(vars(get)) |
| method = getattr(module, action) |
| return method(get) |
|
|
| def set_cert_from_local(self, args): |
| from sslModel import certModel |
| data = certModel.main().get_cert_content(args) |
| if not data['status']: |
| return data |
| args.key = data['content']['key'] |
| args.csr = data['content']['cert'] |
| args.act = 'add' |
| return self.set_mail_certificate_multiple(args) |
|
|
| def get_task_unsubscribe_list(self,args): |
| """ 获取营销任务 退订详情列表 """ |
| p = int(args.p) if 'p' in args else 1 |
| rows = int(args.size) if 'size' in args else 12 |
|
|
| task_id = args.get('task_id', '') |
| if not task_id: |
| return public.return_message(-1, 0, public.lang("The required id parameter is missing")) |
| with public.S("mail_unsubscribe", '/www/vmail/mail_unsubscribe.db') as obj: |
| count = obj.where('task_id', task_id).select() |
| |
| data1 = obj.order('created', 'DESC').where('active', 0).limit(rows, (p - 1) * rows).where('task_id', task_id).group('recipient').select() |
| |
| data2 = obj.order('created', 'DESC').where('active', 0).limit(rows, (p - 1) * rows).where('task_id', task_id).select() |
| result = {} |
|
|
| data = data1 + data2 |
| |
| for entry in data: |
| recipient = entry['recipient'] |
| created = entry['created'] |
|
|
| |
| if recipient not in result or created > result[recipient]['created']: |
| result[recipient] = entry |
|
|
| |
| data = list(result.values()) |
|
|
| return {'data': data, 'total': len(count)} |
|
|