| |
| |
|
|
| import re, json, os, sys, time, socket, requests, glob,shutil |
| import subprocess |
| import dns.resolver |
| import argparse |
| import zipfile |
|
|
| |
|
|
| sys.path.append("class/") |
| from mod.base import public_aap as public |
|
|
| |
| |
|
|
| |
|
|
| if sys.version_info[0] == 3: |
| from importlib import reload |
|
|
| try: |
| from dateutil.parser import parse |
| except: |
| public.ExecShell("btpip install python-dateutil==2.9.0.post0") |
| from dateutil.parser import parse |
|
|
| 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 |
| from mailModel.base import Base |
| from mailModel.mainModel import SendMail |
| import math |
| try: |
| import jwt |
| except: |
| public.ExecShell('btpip install pyjwt') |
| import jwt |
| from datetime import datetime, timedelta |
| from email.utils import make_msgid |
|
|
| class main(Base): |
|
|
| postfix_main_cf = "/etc/postfix/main.cf" |
| domain_restrictions = '/etc/postfix/sender_black' |
| plugin_data = f'/www/server/panel/plugin/mail_sys/data' |
| blcheck_count = f'/www/server/panel/plugin/mail_sys/data/blcheck.json' |
| CONF_DNS_TRIES = 2 |
| CONF_DNS_DURATION = 6 |
| CONF_BLACKLISTS = [ |
| "bl.spamcop.net", |
| "dnsbl.sorbs.net", |
| "multi.surbl.org", |
| "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", |
| "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.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", |
| ] |
| |
| REGEX_IP = r'^\b(?:\d{1,3}\.){3}\d{1,3}\b$' |
| def __init__(self): |
| super().__init__() |
| self.in_bulk_path = '/www/server/panel/data/mail/in_bulk' |
| if not os.path.exists(self.in_bulk_path): |
| os.mkdir(self.in_bulk_path) |
| |
| self.maillog_path = '/var/log/maillog' |
| if "ubuntu" in public.get_linux_distribution().lower(): |
| self.maillog_path = '/var/log/mail.log' |
| |
| self.sent_recipient_path = '/www/server/panel/data/mail/in_bulk/recipient/sent_recipient' |
| if not os.path.exists(self.sent_recipient_path): |
| os.makedirs(self.sent_recipient_path) |
| |
| |
| |
| 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' |
| } |
| if os.path.exists('/www/vmail/'): |
| self.check_table_column() |
| |
| self.get_yesterday_count() |
|
|
| |
| self.restored_domain_send() |
| self.check_new_unsubscribe() |
|
|
| def restored_domain_send(self): |
| restored_domain = '/www/server/panel/plugin/mail_sys/data/restored_domain_send.pl' |
| if os.path.exists(restored_domain): |
| return |
| if self._check_sender_domain_restrictions(): |
| self._domain_restrictions_switch(False) |
| |
| public.writeFile(restored_domain, '1') |
|
|
| 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_table_column(self,): |
| """ |
| @name 检查数据库表或字段是否完整 |
| """ |
| |
| |
| sql1 = '''CREATE TABLE IF NOT EXISTS `temp_email` ( |
| `id` INTEGER PRIMARY KEY AUTOINCREMENT, |
| `name` varchar(255) NULL, -- 邮件名 有模版时为模版名 |
| `type` tinyint(1) NOT NULL DEFAULT 1, -- 拖拽生成1 上传0 |
| `content` text NOT NULL, -- 邮件正文 路径 |
| `render` text NOT NULL, -- html渲染数据 |
| `created` INTEGER NOT NULL, |
| `modified` INTEGER NOT NULL, |
| `is_temp` tinyint(1) NOT NULL DEFAULT 0 -- 是否是模版 |
| );''' |
| with self.M("") as obj: |
| obj.execute(sql1, ()) |
|
|
|
|
| |
| 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 |
| `etypes` varchar(320) NOT NULL DEFAULT '1', -- 邮件类型id 默认为1 多个 |
| `created` INTEGER NOT NULL, |
| `modified` INTEGER NOT NULL, |
| `remark` text, -- 备注 |
| `start_time` INTEGER NOT NULL DEFAULT 0, -- 任务开始时间 |
| `subject` text NULL, -- 邮件主题 改task存 可空 |
| `full_name` varchar(255), -- 新发件人名 改task存 可空 |
| `recipient` text NOT NULL, -- 收件人路径 改task存 |
| `active` tinyint(1) NOT NULL DEFAULT 0 -- 预留字段 |
| );''' |
| with self.M("") as obj: |
| obj.execute(sql2, ()) |
|
|
|
|
| |
|
|
| sql3 = '''CREATE TABLE IF NOT EXISTS `task_count` ( |
| `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, -- dsn |
| `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 |
| );''' |
| with self.M("") as obj: |
| obj.execute(sql3, ()) |
|
|
| |
| with self.M("temp_email") as obj: |
| if not self.check_field_exists(obj, "temp_email", "render"): |
| obj.execute('ALTER TABLE `temp_email` ADD COLUMN `render` text NOT NULL DEFAULT "";') |
| if not self.check_field_exists(obj, "temp_email", "type"): |
| obj.execute('ALTER TABLE `temp_email` ADD COLUMN `type` tinyint(1) NOT NULL DEFAULT 1;') |
| |
| with self.M("temp_email") as obj: |
| if self.check_field_exists(obj, "temp_email", "addresser"): |
| obj.execute('ALTER TABLE `temp_email` DROP COLUMN `addresser`;') |
| if self.check_field_exists(obj, "temp_email", "recipient"): |
| obj.execute('ALTER TABLE `temp_email` DROP COLUMN `recipient`;') |
| if self.check_field_exists(obj, "temp_email", "subject"): |
| obj.execute('ALTER TABLE `temp_email` DROP COLUMN `subject`;') |
| if self.check_field_exists(obj, "temp_email", "subtype"): |
| obj.execute('ALTER TABLE `temp_email` DROP COLUMN `subtype`;') |
| |
| with self.M("email_task") as obj: |
| if not self.check_field_exists(obj, "email_task", "unsubscribe"): |
| obj.execute('ALTER TABLE `email_task` ADD COLUMN `unsubscribe` INTEGER NOT NULL DEFAULT 0;') |
|
|
| if not self.check_field_exists(obj, "email_task", "threads"): |
| obj.execute('ALTER TABLE `email_task` ADD COLUMN `threads` INTEGER NOT NULL DEFAULT 0;') |
| |
| |
| |
| |
| if not self.check_field_exists(obj, "email_task", "start_time"): |
| obj.execute('ALTER TABLE `email_task` ADD COLUMN `start_time` INTEGER NOT NULL DEFAULT 0;') |
| if not self.check_field_exists(obj, "email_task", "remark"): |
| obj.execute('ALTER TABLE `email_task` ADD COLUMN `remark` text;') |
|
|
| |
| |
|
|
| |
| if not self.check_field_exists(obj, "email_task", "etypes"): |
| obj.execute('ALTER TABLE `email_task` ADD COLUMN `etypes` varchar(320) NOT NULL DEFAULT "1";') |
|
|
| if not self.check_field_exists(obj, "email_task", "subject"): |
| obj.execute('ALTER TABLE `email_task` ADD COLUMN `subject` text;') |
|
|
| if not self.check_field_exists(obj, "email_task", "full_name"): |
| obj.execute('ALTER TABLE `email_task` ADD COLUMN `full_name` varchar(255) DEFAULT "";') |
|
|
|
|
| if not self.check_field_exists(obj, "email_task", "recipient"): |
| obj.execute('ALTER TABLE `email_task` ADD COLUMN `recipient` text NOT NULL DEFAULT "";') |
|
|
| |
| |
| |
| |
| |
|
|
| with self.M("task_count") as obj: |
| if not self.check_field_exists(obj, "task_count", "created"): |
| obj.execute('ALTER TABLE `task_count` ADD COLUMN `created` INTEGER NOT NULL DEFAULT 0;') |
|
|
| |
| with self.MD("mail_unsubscribe", "mail_unsubscribe") as obj: |
| if not self.check_field_exists(obj, "mail_unsubscribe", "task_id"): |
| obj.execute('ALTER TABLE `mail_unsubscribe` ADD COLUMN `task_id` INTEGER DEFAULT 0;') |
| def MD(self, table_name, db_key): |
| if db_key not in self.db_files: |
| raise ValueError(f"Unknown database key: {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 check_task_status(self, args): |
| ''' |
| 执行发送邮件的定时任务 |
| :param |
| :return: |
| ''' |
| try: |
| print("|-Prepare to execute the send task") |
| |
| |
| with self.M("email_task") as obj: |
| exits_task = obj.where('task_process =? and pause =?', (1, 0)).count() |
| process0_task = obj.where('task_process =? and pause =?', (0, 0)).select() |
|
|
| if exits_task: |
| |
| print("|-An existing task is being sent ") |
| return False |
| if not process0_task: |
| |
| print("|-There are no tasks to execute") |
| return False |
| |
| cur_time = int(time.time()) |
| send_task_ok = [] |
| for i in process0_task: |
| |
| if i['start_time'] <= cur_time: |
| send_task_ok.append(i) |
|
|
| |
| if send_task_ok: |
| if len(send_task_ok) == 1: |
| task_info = send_task_ok[0] |
| else: |
| task_info = min(send_task_ok, key=lambda x: x['start_time']) |
| else: |
| |
| print("|-No task has reached its dispatch time") |
| return False |
|
|
|
|
|
|
| |
|
|
|
|
| start_mark = '/www/server/panel/plugin/mail_sys/start_Task.pl' |
| SendTaskId = '/www/server/panel/plugin/mail_sys/SendTaskid.pl' |
| _, domain_ = task_info['addresser'].split('@') |
| |
| is_ptr = self._check_ptr_domain(domain_) |
| |
|
|
| |
| if os.path.exists(start_mark): |
| |
| |
| if int(public.readFile(start_mark)) + 86400 < cur_time: |
| |
| |
| public.writeFile(start_mark, str(cur_time)) |
| |
| count_sent = '/www/server/panel/plugin/mail_sys/count_sent_domain.json' |
| os.remove(count_sent) |
| |
| else: |
| |
| if not is_ptr: |
| |
| |
| count_sent = '/www/server/panel/plugin/mail_sys/count_sent_domain.json' |
| count = 0 |
| if os.path.exists(count_sent): |
| data = public.readFile(count_sent) |
| data = json.loads(data) |
| count = sum(domain_data for domain_data in data.values()) |
| |
| |
| if count > 5000: |
| print("|-The execution quota for the day has been used up") |
| |
| return False |
|
|
| public.writeFile(SendTaskId, str(task_info['id'])) |
| else: |
| timestamp = str(int(time.time())) |
| public.writeFile(start_mark, timestamp) |
| |
| public.writeFile(SendTaskId, str(task_info['id'])) |
| |
|
|
| |
| email_info = self.M('temp_email').where('id=?', task_info['temp_id']).find() |
| content_path = email_info['content'] |
|
|
| recipient_path = task_info['recipient'] |
| addresser = task_info['addresser'] |
| subject = task_info.get('subject', '') |
| |
| full_name = task_info.get('full_name', '') |
|
|
| try: |
| content_detail = public.readFile(content_path) |
| content_detail = json.loads(content_detail) |
| except: |
| |
| content_detail = public.readFile(content_path) |
|
|
| task_id = task_info['id'] |
| unsubscribe = task_info['unsubscribe'] |
|
|
| threads = task_info['threads'] |
| etypes = task_info['etypes'] |
|
|
| |
| recipient_analysis = {} |
| try: |
| data = public.readFile(recipient_path) |
| recipient_analysis = json.loads(data) |
| except: |
| print(public.get_error_info()) |
| public.print_log(public.get_error_info()) |
| |
|
|
| |
| data = self.M('mailbox').where('username=?', addresser).field('password_encode,full_name').find() |
| password = self._decode(data['password_encode']) |
| |
| if not full_name: |
| full_name = data['full_name'] |
|
|
| other_today = { |
| 'gmail.com': {"count": 0, "info": []}, |
| 'googlemail.com': {"count": 0, "info": []}, |
| 'hotmail.com': {"count": 0, "info": []}, |
| 'outlook.com': {"count": 0, "info": []}, |
| 'yahoo.com': {"count": 0, "info": []}, |
| 'icloud.com': {"count": 0, "info": []}, |
| 'other': {"count": 0, "info": []}, |
| } |
| |
| is_record = task_info['is_record'] |
| |
| |
| |
| |
| args1 = public.dict_obj() |
| args1.addresser = addresser |
| args1.password = password |
| args1.full_name = full_name |
| args1.subject = subject |
| args1.content_detail = content_detail |
| args1.is_record = is_record |
| args1.unsubscribe = unsubscribe |
| args1.task_id = task_id |
| args1.etypes = etypes |
|
|
| |
| p_list = [] |
| if not is_ptr: |
| |
| |
| send_today = { |
| 'gmail.com': {"count": 0, "info": []}, |
| 'googlemail.com': {"count": 0, "info": []}, |
| 'hotmail.com': {"count": 0, "info": []}, |
| 'outlook.com': {"count": 0, "info": []}, |
| 'yahoo.com': {"count": 0, "info": []}, |
| 'icloud.com': {"count": 0, "info": []}, |
| 'other': {"count": 0, "info": []}, |
| } |
| |
| for domain, details in recipient_analysis.items(): |
| today_count = 0 |
| |
| count = self._get_count_limit(domain) |
| |
| |
| if count > 5000: |
| |
| today_count = 0 |
| |
| elif details['count'] + count > 5000: |
| |
| today_count = 5000 - count |
| else: |
| today_count = 5000 |
|
|
| if today_count != 0: |
| if details['count'] < today_count: |
| send_today[domain] = details |
| other_today[domain] = {"count": 0, "info": []} |
| else: |
| send_today[domain] = {"count": len(details[:today_count]), |
| "info": details[:today_count]} |
| other_today[domain] = {"count": len(details[today_count:]), "info": details[today_count:]} |
| else: |
| send_today[domain] = {"count": 0, "info": []} |
| other_today[domain] = details |
|
|
| |
| try: |
| import random |
| listall = [] |
| for domain, detail in send_today.items(): |
| listall += detail['info'] |
| if len(listall) == 0: |
| |
| with self.M("email_task") as obj: |
| obj.where('id=?', task_info['id']).update({'task_process': 2}) |
| return |
| random.shuffle(listall) |
| |
| |
| public.writeFile(recipient_path, json.dumps(other_today)) |
| |
|
|
| args1.listall = listall |
| args1.threads = 1 |
| |
| p1_list = self.send_emails_split(args1) |
| p_list.extend(p1_list) |
| except Exception as ex: |
| print(public.get_error_info()) |
| public.print_log(public.get_error_info()) |
| |
| |
| if os.path.exists(start_mark): |
| os.remove(start_mark) |
| print("|-Installment delivery failed - error: {}".format(ex)) |
| |
| return False |
|
|
| else: |
| |
| |
| with self.M("email_task") as obj: |
| obj.where('id=?', task_info['id']).update({'task_process': 1}) |
| try: |
| import random |
| listall = [] |
| for domain, detail in recipient_analysis.items(): |
| listall += detail['info'] |
| if len(listall) == 0: |
| |
| with self.M("email_task") as obj: |
| obj.where('id=?', task_info['id']).update({'task_process': 2}) |
|
|
| return |
| |
| random.shuffle(listall) |
|
|
| |
| public.writeFile(recipient_path, json.dumps(other_today)) |
|
|
| args1.listall = listall |
| args1.threads = int(threads) |
|
|
| |
| p1_list = self.send_emails_split(args1) |
| p_list.extend(p1_list) |
|
|
| except Exception as ex: |
| print(public.get_error_info()) |
| public.print_log(public.get_error_info()) |
| |
| |
| if os.path.exists(start_mark): |
| os.remove(start_mark) |
| |
| with self.M("email_task") as obj: |
| obj.where('id=?', task_info['id']).update({'task_process': 0}) |
|
|
| print("|-Failed to send - error: {}".format(ex)) |
| |
| return False |
|
|
| |
| |
| for p in p_list: |
| p.join() |
|
|
|
|
| other_todays = {} |
| try: |
| data = public.readFile(recipient_path) |
| other_todays = json.loads(data) |
| except: |
| print(public.get_error_info()) |
| public.print_log(public.get_error_info()) |
|
|
| if all(value['count'] == 0 for value in other_todays.values()): |
| |
| with self.M("email_task") as obj: |
| obj.where('id=?', task_info['id']).update({'task_process': 2}) |
| |
| else: |
| |
| with self.M("email_task") as obj: |
| obj.where('id=?', task_info['id']).update({'task_process': 0}) |
| |
| return public.returnMsg(True, '已完成发送任务') |
| except: |
| print(public.get_error_info()) |
| public.print_log(public.get_error_info()) |
|
|
| |
| |
| def send_emails_split(self, args): |
| |
| task_id = args.task_id |
| listall = args.listall |
| unsubscribe = args.unsubscribe |
| threads = args.threads |
| addresser = args.addresser |
| password = args.password |
| full_name = args.full_name |
| subject = args.subject |
| content_detail = args.content_detail |
| is_record = args.is_record |
| etypes = args.etypes |
| |
| total_recipients = len(listall) |
| if total_recipients > 10000: |
| max_batches = int(threads) |
| if max_batches == 0: |
| |
| if total_recipients > 50000: |
| max_batches = 5 |
| else: |
| max_batches = 3 |
| else: |
| |
| max_batches = 1 |
|
|
| |
| |
| |
|
|
| |
| batch_size = math.ceil(total_recipients / max_batches) |
|
|
| |
| |
| p_all = [] |
|
|
| for i in range(max_batches): |
| start_idx = i * batch_size |
| end_idx = min(total_recipients, (i + 1) * batch_size) |
| batch_recipients = listall[start_idx:end_idx] |
| |
| |
| recipients = {"count": len(batch_recipients), "info": batch_recipients} |
| args.recipients = recipients |
| |
| if unsubscribe: |
| |
|
|
| p = self.run_thread(self._send_email_all_unsubscribe, (recipients, addresser, password, full_name, subject, content_detail, is_record, task_id, etypes)) |
| |
| else: |
|
|
| p = self.run_thread(self._send_email_all, (recipients, addresser, password, full_name, subject, content_detail, is_record, task_id)) |
| |
|
|
| p_all.append(p) |
|
|
| return p_all |
|
|
| def run_thread(self, fun, args=(), daemon=False): |
| ''' |
| @name 使用线程执行指定方法 |
| @author hwliang<2020-10-27> |
| @param fun {def} 函数对像 |
| @param args {tuple} 参数元组 |
| @param daemon {bool} 是否守护线程 |
| @return 线程 |
| ''' |
| import threading |
| p = threading.Thread(target=fun, args=args) |
| p.setDaemon(daemon) |
| p.start() |
| return p |
|
|
| |
| def check_task_finish(self, args=None): |
| ''' |
| 发送完毕后 处理发送日志 定时任务 |
| :param |
| :return: |
| ''' |
| |
|
|
| |
| LastTaskId = '/www/server/panel/plugin/mail_sys/LastTaskid.pl' |
| SendTaskId = '/www/server/panel/plugin/mail_sys/SendTaskid.pl' |
| if not os.path.exists(SendTaskId): |
| print("|- There are no tasks to work on") |
| |
| return False |
|
|
| |
| task_id = int(public.readFile(SendTaskId)) |
| cur_time = int(time.time()) |
| with self.M("email_task") as obj: |
| exits_task = obj.where('task_process !=?', (0,)).select() |
| process1_task = obj.where('task_process =?', (1,)).count() |
|
|
| if not process1_task: |
| if not obj.where('task_process !=?', (2,)).count(): |
| path = '/www/server/panel/plugin/mail_sys/data/quota_occupation.json' |
| if os.path.exists(path): |
| os.remove(path) |
|
|
| |
| last_task = obj.order('created desc').where('task_process =?', (2,)).find() |
| if cur_time-600 > last_task['created']: |
| if os.path.exists(LastTaskId): |
| os.remove(LastTaskId) |
| |
| print("|-There are no tasks to handle 1") |
| return False |
|
|
| if not exits_task: |
| |
| print("|-There are no tasks to handle 2") |
| return False |
| ids = [i['id'] for i in exits_task] |
|
|
|
|
| |
| task_switch = False |
| if os.path.exists(LastTaskId): |
| last_id = int(public.readFile(LastTaskId)) |
| if task_id != last_id: |
| task_switch = True |
| |
| |
| public.writeFile(LastTaskId, str(task_id)) |
| |
| task_id = last_id |
| else: |
| |
| |
| public.writeFile(LastTaskId, str(task_id)) |
|
|
|
|
|
|
| if task_id not in ids: |
| |
| public.print_log("|-The id is not in the scope of processing") |
| return False |
| with self.M("email_task") as obj: |
| |
| task_info = obj.where('id =?', (task_id,)).find() |
|
|
| |
| if not task_info: |
| print("|-There are currently no tasks") |
| |
| return False |
|
|
| |
| |
| error_log = "/www/server/panel/data/mail/in_bulk/errlog/task_{}.log".format(task_id) |
|
|
|
|
| |
| self._mail_error_log(error_log, task_id) |
| print("|-Logging completion") |
| |
| |
|
|
| |
| |
| if task_info['task_process'] == 2 or task_switch: |
| |
|
|
| |
| |
| |
|
|
| |
| |
|
|
| |
| self.run_thread(self.handle_abnormal_recipient, (task_id,)) |
|
|
| print("|-The processing tag has been removed") |
| |
| return True |
|
|
| def handle_abnormal_recipient(self, task_id): |
|
|
| database_path = f'/www/vmail/bulk/task_{task_id}.db' |
| |
| with public.S("task_count", database_path) as obj: |
| |
| errlist = obj.where('status !=?', ('sent',)).select() |
|
|
| |
| with self.M('email_task') as obj: |
| task_info = obj.where('id=?', task_id).field('temp_id,created,subject,created,remark').find() |
|
|
|
|
| task_name = task_info.get('subject', '') + '_' + task_info.get('remark', '') + '_' + str(task_info['created']) |
| |
| if errlist: |
| err_recipient = [i['recipient'] for i in errlist] |
| exist_recipient = [] |
| |
| with public.S("abnormal_recipient", '/www/vmail/abnormal_recipient.db') as obj1: |
| existlist = obj1.where_in('recipient', err_recipient).select() |
| |
| if existlist: |
| exist_recipient = [i['recipient'] for i in existlist] |
| for i in existlist: |
| obj1.where('id=?', i['id']).update({'count': i['count'] + 1}) |
|
|
| |
| insert_data = [] |
| for i in errlist: |
| if i['recipient'] not in exist_recipient: |
| insert_data.append({ |
| "created": int(time.time()), |
| "recipient": i['recipient'], |
| "status": i['status'], |
| "task_name": task_name, |
| "count": 1, |
| }) |
| if insert_data: |
| aa = obj1.insert_all(insert_data, option='IGNORE') |
|
|
|
|
|
|
| def _read_recipient_file(self,file_path): |
| """读取收件人文件 兼容json和普通类型""" |
|
|
| if file_path.endswith('.json'): |
| try: |
| emails = json.loads(public.readFile(file_path)) |
| return emails, None |
| except Exception as e: |
| return None, f'从文件读取json内容失败: {e}' |
| else: |
| try: |
| with open(file_path, 'r') as file: |
| emails = file.read().splitlines() |
| return emails, None |
| except Exception as e: |
| return None, f'从文件读取文本内容失败: {e}' |
|
|
| |
| def processing_recipient(self, args): |
| ''' |
| 导入收件人 |
| :param file etype(邮件类型 用于筛选退订用户) |
| :return: |
| ''' |
| args.file = args.get('file/s', '') |
|
|
| if not os.path.exists("{}/recipient".format(self.in_bulk_path)): |
| os.mkdir("{}/recipient".format(self.in_bulk_path)) |
| file_path = "{}/recipient/{}".format(self.in_bulk_path, args.file) |
|
|
| if not args.file: |
| return public.returnMsg(False, '参数错误') |
|
|
| if not os.path.exists(file_path): |
| return public.returnMsg(False, '文件不存在') |
| emails = [] |
| |
| try: |
| emails, err = self._read_recipient_file(file_path) |
| |
| if err: |
| return public.returnMsg(False, err) |
| |
| emails = list(map(lambda x: x.strip(),filter(lambda x: x != "", emails))) |
|
|
| |
| from collections import Counter |
| email_counts = Counter(emails) |
| duplicates = [email for email, count in email_counts.items() if count > 1] |
| |
|
|
| |
| emails = list(set(emails)) |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
| return public.returnMsg(False, e) |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
|
|
| |
| recipient_analysis = {} |
|
|
| verify_results = {"success": {}, "failed": {}} |
|
|
|
|
| |
| |
| with self.MD("mail_unsubscribe", "mail_unsubscribe") as obj: |
| mail_unsubscribe = obj.where('etype=?', 0).select() |
| blacklist = [i['recipient'] for i in mail_unsubscribe] |
| blacklist_count = 0 |
| for email in emails: |
| |
| |
| |
| |
| |
| |
| |
| |
| if blacklist: |
| if email in blacklist: |
| |
| blacklist_count += 1 |
| continue |
| |
| |
| |
| |
| |
| |
|
|
| local_part, domain = email.lower().split('@') |
| |
| domain_key = domain |
| if not recipient_analysis.get(domain_key): |
| recipient_analysis[domain_key] = {"count": 0, "info": []} |
| recipient_analysis[domain_key]["info"].append(email) |
| recipient_analysis[domain_key]["count"] += 1 |
| verify_results["success"][email] = "Common post office" if domain != 'other' else "Other domains" |
|
|
| |
| recipient_path = "{}/recipient/verify_{}".format(self.in_bulk_path, args.file) |
|
|
| public.writeFile(recipient_path, public.GetJson(recipient_analysis)) |
|
|
| return public.returnMsg(True, |
| public.lang('导入成功, 跳过 {}取消订阅用户,重复的邮件地址:{}',blacklist_count, len(duplicates))) |
|
|
| |
| def get_recipient_data(self, args): |
| ''' |
| 获取发送预计完成时间 |
| :param file |
| :return: |
| ''' |
| if not args.file: |
| return public.returnMsg(False, '参数错误') |
| recipient_path = "{}/recipient/verify_{}".format(self.in_bulk_path, args.file) |
| try: |
| data = public.readFile(recipient_path) |
| recipient_analysis = json.loads(data) |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
| return public.returnMsg(False, '文件内容异常或格式错误: {}'.format(e)) |
|
|
| return public.returnMsg(True, recipient_analysis) |
|
|
| |
| def add_task(self, args): |
| ''' |
| 添加批量发送任务 |
| :param args: |
| :return: |
| ''' |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| try: |
| if not hasattr(args, 'addresser') or args.get('addresser/s', '') == '': |
| return public.returnMsg(False, public.lang('参数错误:addresser')) |
|
|
| if not hasattr(args, 'task_name') or args.get('task_name/s', '') == '': |
| return public.returnMsg(False, public.lang('参数错误:task_name')) |
| task_name = args.get('task_name/s', '') |
| |
| if not hasattr(args, 'etypes') or args.get('etypes', '') == '': |
| etypes = '1' |
| else: |
| etypes = args.etypes |
|
|
| etype_list = etypes.split(',') |
| |
| with public.S("mail_unsubscribe", '/www/vmail/mail_unsubscribe.db') as obj: |
| count = obj.where_in('etype', etype_list).where('active', 1).count() |
| if not count: |
| return public.returnMsg(False, public.lang('所选联系人列表为空')) |
|
|
| |
| if not hasattr(args, 'temp_id') or args.get('temp_id/d', 0) == 0: |
| return public.returnMsg(False, '参数错误: temp_id') |
| temp_id = args.temp_id |
|
|
|
|
| |
| namemd5 = public.md5(task_name) |
| |
| recipient_path = "{}/recipient/{}_verify_{}".format(self.in_bulk_path, namemd5, etypes) |
| |
| recipient_count, black_num, abnormal_num = self.processing_recipient_v2(recipient_path, etype_list) |
| if recipient_count == 0: |
| return public.returnMsg(False, public.lang('没有符合条件的收件人, 黑名单 {} 异常邮箱 {}', black_num, abnormal_num)) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| if not hasattr(args, 'is_record') or args.get('is_record/d', 0) == 0: |
| is_record = 0 |
| else: |
| is_record = 1 |
| |
| if not hasattr(args, 'unsubscribe') or args.get('unsubscribe/d', 0) == 0: |
| unsubscribe = 0 |
| else: |
| unsubscribe = 1 |
| |
| if not hasattr(args, 'threads') or args.get('threads/d', 0) == 0: |
| threads = 0 |
| else: |
| threads = int(args.threads) |
| if threads > 10: |
| return public.returnMsg(False, '线程数不能超过10') |
|
|
|
|
| |
| if not hasattr(args, 'start_time'): |
| start_time = int(time.time()) |
| else: |
| start_time = int(args.start_time) |
|
|
| |
| |
| |
| |
| |
| pause = 0 |
|
|
| |
| if not hasattr(args, 'full_name') or args.get('full_name', '') == '': |
| full_name = '' |
| else: |
| full_name = args.get('full_name', '') |
| |
| remark = args.get('remark', '') |
| addresser = args.get('addresser/s', '') |
| subject = args.get('subject/s', '') |
| task_process = args.get('task_process', 0) |
|
|
|
|
| |
| data = self.M('mailbox').where('username=?', addresser).find() |
| if not data: |
| return public.returnMsg(False, '邮件地址不存在') |
|
|
| |
| self.init_send() |
| |
| timestamp = int(time.time()) |
| task_id = 0 |
|
|
| try: |
|
|
| |
| task_id = self.M('email_task').add( |
| 'task_name,addresser,recipient_count,task_process,pause,temp_id,is_record,unsubscribe,threads,created,modified,start_time,remark,etypes,recipient,subject,full_name', |
| (task_name, addresser, recipient_count, task_process, pause, temp_id, is_record, unsubscribe, threads, |
| timestamp, timestamp, start_time, remark, etypes, recipient_path, subject,full_name)) |
|
|
|
|
| |
| error_log = "/www/server/panel/data/mail/in_bulk/errlog/task_{}.log".format(task_id) |
| if not os.path.exists(error_log): |
| public.WriteFile(error_log, '') |
|
|
| |
| self._task_mail_send1() |
| self._task_mail_send2() |
| |
| |
| |
| self.create_task_database(task_id) |
| |
| public.set_module_logs('mailModel', 'add_task', 1,) |
| public.set_module_logs('mailModel', 'bulk_emails', recipient_count) |
| return public.returnMsg(True, '任务添加成功') |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
| |
| |
| self.M('email_task').where('id=?', task_id).delete() |
| |
| database_path = f'/www/vmail/bulk/task_{task_id}.db' |
| if os.path.exists(database_path): |
| os.remove(database_path) |
| return public.returnMsg(False, '任务添加失败: [{0}]'.format(str(e))) |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
|
|
| |
| |
| def send_mail_test(self, args): |
| |
| if not hasattr(args, 'temp_id') or args.get('temp_id/d', 0) == 0: |
| return public.returnMsg(False, '参数错误: temp_id') |
| temp_id = args.get('temp_id', 0) |
|
|
| email_info = self.M('temp_email').where('id=?', temp_id).find() |
| if not email_info: |
| return public.returnMsg(False, public.lang('模板不存在')) |
|
|
| content_path = email_info['content'] |
|
|
| if os.path.exists(content_path): |
| content = public.readFile(content_path) |
|
|
| else: |
| return public.returnMsg(False, public.lang('{}文件不存在', content_path)) |
|
|
| subject = args.get('subject', '') |
| |
|
|
| |
| 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 = args.mail_to.split(',') |
|
|
| _, domain = mail_from.split('@') |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| files = json.loads(args.files) if 'files' in args else [] |
| |
| if not isinstance(mail_to, list): |
| return public.returnMsg(False, '收件人不是list') |
| if len(mail_to) == 0: |
| return 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 result |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
| return public.returnMsg(False, public.lang('发送失败, 错误原因 [{0}]', str(e))) |
|
|
| def import_contacts(self, args): |
| ''' |
| 导入收件人到联系人列表 |
| :param file str (收件人文件名) |
| :param etypes str (联系人类型 多个逗号隔开) 多选分类 每个分类都导入 |
| :param active int (0 退订 1订阅) 暂不使用,默认订阅类型 |
| :return: |
| ''' |
| file = args.get('file/s', '') |
| |
|
|
| if not file: |
| return public.returnMsg(False, public.lang('参数错误')) |
|
|
| if not hasattr(args, 'mail_type'): |
| return public.returnMsg(False, public.lang('参数错误: mail_type')) |
|
|
| created = int(time.time()) |
| insert = { |
| 'created': created, |
| 'mail_type': args.mail_type, |
| } |
|
|
| with self.M('mail_type') as obj: |
| exit = obj.where('mail_type =?',(args.mail_type,)).count() |
| if exit: |
| return public.returnMsg(False, public.lang('这种类型已经存在')) |
| etype = obj.insert(insert) |
|
|
| if not os.path.exists("{}/recipient".format(self.in_bulk_path)): |
| os.mkdir("{}/recipient".format(self.in_bulk_path)) |
| file_path = "{}/recipient/{}".format(self.in_bulk_path, file) |
| if not os.path.exists(file_path): |
| return public.returnMsg(False, public.lang('文件不存在')) |
|
|
|
|
| |
| try: |
| emails, err = self._read_recipient_file(file_path) |
| if err: |
| return public.returnMsg(False, err) |
| |
| emails = list(map(lambda x: x.strip(),filter(lambda x: x != "", emails))) |
|
|
| |
| from collections import Counter |
| email_counts = Counter(emails) |
| duplicates = [email for email, count in email_counts.items() if count > 1] |
|
|
| |
| emails = list(set(emails)) |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
| return public.returnMsg(False, e) |
|
|
| import_info = [] |
|
|
| |
| |
|
|
| etype = int(etype) |
|
|
| with public.S("mail_unsubscribe", '/www/vmail/mail_unsubscribe.db') as obj: |
| |
| unsubscribe_list = obj.where_in('recipient', emails).where('etype', etype).where('active', 0).select() |
| |
| subscribe_list = obj.where_in('recipient', emails).where('etype', etype).where('active', 1).select() |
|
|
| unsubscribes = [i['recipient'] for i in unsubscribe_list] |
| subscribes = [i['recipient'] for i in subscribe_list] |
| import itertools |
| merged_list = list(itertools.chain(unsubscribes, subscribes)) |
|
|
| |
| unup_num = obj.where_in('recipient', unsubscribes).update({"active": 1}) |
|
|
| |
| add_emails = [i for i in emails if i not in merged_list] |
| insert_data = [] |
|
|
| for i in add_emails: |
| insert_info = { |
| 'created': created, |
| 'recipient': i, |
| 'etype': etype, |
| 'active': 1, |
| 'task_id': 0, |
| } |
| insert_data.append(insert_info) |
| add_num = obj.insert_all(insert_data, option='IGNORE') |
| import_info.append({ |
| "etype": etype, |
| "mail_type": args.mail_type, |
| "unup_num": unup_num, |
| "add_num": add_num, |
| }) |
| data = { |
| "duplicates": duplicates, |
| "import_info": import_info, |
| } |
| |
| |
| return public.returnMsg(True, public.lang('成功添加邮箱{}个, 失败{}个, 重复邮箱{}个',add_num,unup_num,len(duplicates))) |
|
|
|
|
| def import_contacts_etypes(self, args): |
| ''' |
| 导入收件人到联系人列表 |
| :param file str (收件人文件名) |
| :param etypes str (联系人类型 多个逗号隔开) 多选分类 每个分类都导入 |
| :param active int (0 退订 1订阅) 暂不使用,默认订阅类型 |
| :return: |
| ''' |
| file = args.get('file/s', '') |
| etypes = args.get('etypes/s', '') |
|
|
| if not file: |
| return public.returnMsg(False, public.lang('参数错误:file')) |
| if not etypes: |
| return public.returnMsg(False, public.lang('参数错误:etypes')) |
|
|
| if not os.path.exists("{}/recipient".format(self.in_bulk_path)): |
| os.mkdir("{}/recipient".format(self.in_bulk_path)) |
| file_path = "{}/recipient/{}".format(self.in_bulk_path, file) |
| |
| if not os.path.exists(file_path): |
| return public.returnMsg(False, public.lang('文件不存在')) |
|
|
| etype_list = etypes.split(",") |
|
|
| |
| try: |
| emails, err = self._read_recipient_file(file_path) |
| if err: |
| return public.returnMsg(False, err) |
| |
| emails = list(map(lambda x: x.strip(),filter(lambda x: x != "", emails))) |
|
|
| |
| from collections import Counter |
| email_counts = Counter(emails) |
| duplicates = [email for email, count in email_counts.items() if count > 1] |
|
|
| |
| emails = list(set(emails)) |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
| return public.returnMsg(False, e) |
|
|
| with self.M('mail_type') as obj: |
| data_list = obj.select() |
| types = {str(item["id"]): item["mail_type"] for item in data_list} |
| import_info = [] |
|
|
| |
| |
| for etype in etype_list: |
| etype = int(etype) |
| if not types.get(str(etype), None): |
| continue |
|
|
| with public.S("mail_unsubscribe", '/www/vmail/mail_unsubscribe.db') as obj: |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
|
|
| |
| |
| unup_num = 0 |
|
|
| |
| |
| insert_data = [] |
| created = int(time.time()) |
| for i in emails: |
| insert_info = { |
| 'created': created, |
| 'recipient': i, |
| 'etype': etype, |
| 'active': 1, |
| 'task_id': 0, |
| } |
| insert_data.append(insert_info) |
| add_num = obj.insert_all(insert_data, option='IGNORE') |
| import_info.append({ |
| "etype": etype, |
| "mail_type": types[str(etype)], |
| "unup_num": unup_num, |
| "add_num": add_num, |
| }) |
| data = { |
| "duplicates":duplicates, |
| "import_info":import_info, |
| } |
| |
| |
| return public.returnMsg(True, public.lang('成功添加邮箱{}个, 失败{}个, 重复邮箱{}个',add_num,unup_num,len(duplicates))) |
|
|
| |
| def processing_recipient_v2(self, recipient_path, etype_list): |
| ''' |
| 导入收件人 (收件人写入文件时去重) |
| |
| :param str recipient_path 处理后的收件人文件路径 |
| :param int etype_list(邮件类型 [1,2,3]) |
| :return: int,int,int 收件人数量 ,黑名单数量,异常邮箱数量 |
| ''' |
|
|
| |
| try: |
| 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] |
| |
| emails = list(set(emails)) |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
| return public.returnMsg(False, e) |
|
|
| recipient_analysis = { |
| 'gmail.com': {"count": 0, "info": []}, |
| 'googlemail.com': {"count": 0, "info": []}, |
|
|
| 'hotmail.com': {"count": 0, "info": []}, |
| 'outlook.com': {"count": 0, "info": []}, |
|
|
| 'yahoo.com': {"count": 0, "info": []}, |
|
|
| 'icloud.com': {"count": 0, "info": []}, |
|
|
| 'other': {"count": 0, "info": []}, |
| } |
|
|
| verify_results = {"success": {}, "failed": {}} |
|
|
| |
| blacklist = [] |
| |
| etype_list.append(0) |
| |
| with public.S("mail_unsubscribe", '/www/vmail/mail_unsubscribe.db') as obj: |
| unemails = obj.where_in('etype', etype_list).where('active', 0).select() |
|
|
| if unemails: |
| blacklist = [i['recipient'] for i in unemails] |
| abnormal_recipient = self.get_abnormal_recipient() |
|
|
| blacklist_count = 0 |
| abnormal_count = 0 |
| for email in emails: |
| |
| if blacklist: |
| if email in blacklist: |
| |
| blacklist_count += 1 |
| continue |
| |
| if abnormal_recipient: |
| if email in abnormal_recipient: |
| |
| abnormal_count += 1 |
| continue |
| local_part, domain = email.lower().split('@') |
| domain_key = domain if domain in recipient_analysis else 'other' |
| recipient_analysis[domain_key]["info"].append(email) |
| recipient_analysis[domain_key]["count"] += 1 |
| verify_results["success"][email] = "Common post office" if domain != 'other' else "Other domains" |
|
|
| |
| public.writeFile(recipient_path, public.GetJson(recipient_analysis)) |
|
|
| |
| total_count = sum(domain_data["count"] for domain_data in recipient_analysis.values()) |
|
|
| return total_count, blacklist_count,abnormal_count |
|
|
| |
| def pause_task(self, args): |
| ''' |
| 暂停发送任务 判断状态为执行中的可以暂停 task_process 1 |
| :param args: task_id 任务id; pause 1暂停 0 重启 |
| :return: |
| ''' |
|
|
| if not hasattr(args, 'task_id') or args.get('task_id/d', 0) == 0: |
| return public.returnMsg(False, '参数错误') |
| if not hasattr(args, 'pause'): |
| return public.returnMsg(False, '参数错误') |
| task_info = self.M('email_task').where('id=?', args.task_id).find() |
| pause = int(args.pause) |
| if pause == 1 and task_info['task_process'] != 1: |
| return public.returnMsg(False, '只能暂停执行中的任务') |
|
|
| self.M('email_task').where('id=?', args.task_id).update({'pause': pause}) |
| info = { |
| "1": "暂停", |
| |
| "0": "发送" |
| } |
| return public.returnMsg(True, '{}成功'.format(info[args.pause])) |
|
|
| |
| def get_task_list(self, args): |
| ''' |
| 任务列表 |
| :param args: |
| :return: |
| ''' |
|
|
| 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('email_task').count() |
| page_data = public.get_page(count, p=p, rows=rows, callback=callback) |
|
|
| try: |
| task_list = self.M('email_task').order('created desc').limit( |
| page_data['shift'] + ',' + page_data['row']).select() |
| email_list = self.M('temp_email').order('created desc').select() |
| if not task_list: |
| return public.returnMsg(True, {'data': [], 'page': page_data['page']}) |
| email_dict = {item['id']: item for item in email_list} |
|
|
| |
| with self.M('mail_type') as obj: |
| data_list = obj.select() |
| types = {str(item["id"]): item["mail_type"] for item in data_list} |
| |
| types['0'] = "Unsubscribe all" |
| for task in task_list: |
| temp_id = task['temp_id'] |
| task_id = task['id'] |
| |
| database_path = f'/www/vmail/bulk/task_{task_id}.db' |
| if os.path.exists(database_path): |
| with public.S("task_count", database_path) as obj: |
| count = obj.where('status !=?', 'sent').count() |
|
|
| else: |
| recipients = self.M('task_count').where('task_id=?', task_id).select() |
| unique_recipients = set(r['recipient'] for r in recipients) |
| count = len(unique_recipients) |
|
|
| task['count'] = {"error_count": count} |
| |
| if temp_id in email_dict: |
| task['email_info'] = email_dict[temp_id] |
|
|
| |
| etype_list = task['etypes'].split(",") |
| etype_info = [] |
| for i in etype_list: |
| if types.get(str(i), None): |
| |
| etype_info.append({str(i): types[str(i)]}) |
| task['mail_type'] = etype_info |
|
|
| |
| if task['task_process'] == 2: |
| task['progress'] = 100 |
| task['delivered'] = task['recipient_count'] |
| else: |
| sentcount = self.get_send_progress(task_id) |
| if sentcount > task['recipient_count']: |
| sentcount = task['recipient_count'] |
|
|
| task['progress'] = round(sentcount / task['recipient_count'] * 100, 2) if task['recipient_count'] > 0 else 0 |
| task['delivered'] = sentcount |
|
|
| |
| task['error_log'] = "/www/server/panel/data/mail/in_bulk/errlog/task_{}.log".format(task_id) |
| if not os.path.exists("/www/server/panel/data/mail/in_bulk/errlog/task_{}.log".format(task_id)): |
| task['error_log'] = "/www/server/panel/data/mail/in_bulk/errlog/{}_{}.log".format(task['task_name'],task_id) |
| sent_recipient_path = f"{self.sent_recipient_path}/toRecipient_{task_id}.log" |
| task['sent_recipient_file'] = sent_recipient_path |
| if not os.path.exists(task['error_log']): |
| public.WriteFile(task['error_log'], '') |
| return public.returnMsg(True, {'data': task_list, 'page': page_data['page']}) |
|
|
| except Exception as e: |
| public.print_log(public.get_error_info()) |
|
|
| def get_task_all(self, args): |
| ''' |
| 获取全部群发任务 |
| :param args: |
| :return: |
| ''' |
|
|
| try: |
| task_list = self.M('email_task').order('created desc').field('id,task_name,subject,created').select() |
|
|
| return public.returnMsg(True, task_list) |
|
|
| except Exception as e: |
| public.print_log(public.get_error_info()) |
|
|
|
|
|
|
| |
| def delete_task(self, args): |
| ''' |
| 删除任务 |
| :param args: task_id 任务id |
| :return: |
| ''' |
| if not hasattr(args, 'task_id') or args.get('task_id/d', 0) == 0: |
| return public.returnMsg(False, '参数错误') |
| task_info = self.M('email_task').where('id=?', args.task_id).find() |
|
|
| |
| error_log = "/www/server/panel/data/mail/in_bulk/errlog/task_{}.log".format(task_info['id']) |
| if os.path.exists(error_log): |
| os.remove(error_log) |
| |
| error_log1 = "/www/server/panel/data/mail/in_bulk/errlog/{}_{}.log".format(task_info['task_name'], task_info['id']) |
| if os.path.exists(error_log1): |
| os.remove(error_log1) |
| try: |
| self.M('email_task').where('id=?', task_info['id']).delete() |
| self.M('task_count').where('task_id=?', task_info['id']).delete() |
|
|
| except: |
| public.print_log(public.get_error_info()) |
| |
| start_mark = '/www/server/panel/plugin/mail_sys/start_Task.pl' |
| start_send = '/www/server/panel/plugin/mail_sys/start_Send.pl' |
| if os.path.exists(start_mark): |
| os.remove(start_mark) |
| if os.path.exists(start_send): |
| os.remove(start_send) |
|
|
| |
| |
| |
| database_path = f'/www/vmail/bulk/task_{args.task_id}.db' |
| if os.path.exists(database_path): |
| os.remove(database_path) |
|
|
| return public.returnMsg(True, '删除成功') |
|
|
| |
| def get_log_rank(self, args): |
| ''' |
| 获取错误排行 |
| :param args: |
| task_id 任务id |
| type 类型 domain 域名排行 status 错误类型排行 |
| :return: |
| ''' |
|
|
| if not hasattr(args, 'type'): |
| return public.returnMsg(False, '参数错误') |
|
|
| types = args.type |
| if types == "domain": |
| field = "domain" |
| else: |
| field = "status" |
|
|
| |
| task_id = args.task_id |
| database_path = f'/www/vmail/bulk/task_{task_id}.db' |
| if os.path.exists(database_path): |
| with public.S("task_count", database_path) as obj: |
| rank_list = obj.group(field).field(field, 'count(*) as `count`').where('status !=?', 'sent').select() |
| else: |
|
|
| try: |
| query = ''' |
| SELECT {group_by_field}, COUNT(*) as count |
| FROM task_count |
| WHERE task_id = ? |
| GROUP BY {group_by_field} |
| ORDER BY count DESC |
| LIMIT 10; |
| '''.format(group_by_field=field) |
|
|
| params = (args.task_id,) |
| |
| results = self.M('task_count').query(query, params) |
| |
| |
| rank_list = [] |
| for value, count in results: |
| |
| |
| |
| rank_list.append({ |
| field: value, |
| "count": count, |
| }) |
| except: |
| rank_list = [] |
|
|
| return public.returnMsg(True, rank_list) |
|
|
| def get_log_list(self, args): |
| ''' |
| 获取错误详情 |
| :param args: task_id 任务id |
| :return: |
| ''' |
| if not hasattr(args, 'task_id') or args.get('task_id/d', 0) == 0: |
| return public.returnMsg(False, '参数错误') |
|
|
| p = int(args.page) if 'page' in args else 1 |
| rows = int(args.size) if 'size' in args else 10 |
| |
|
|
| if not hasattr(args, 'type'): |
| return public.returnMsg(False, '参数错误') |
| if not hasattr(args, 'value'): |
| return public.returnMsg(False, '参数错误') |
|
|
| types = args.type |
| value = args.value |
| if types == "domain": |
| fields = "domain=?" |
| else: |
| fields = "status=?" |
|
|
| |
| task_id = args.task_id |
| database_path = f'/www/vmail/bulk/task_{task_id}.db' |
| if os.path.exists(database_path): |
| with public.S("task_count", database_path) as obj: |
| count = obj.where(fields, value).where('status !=?', 'sent').count() |
| error_list = obj.where(fields, value).where('status !=?', 'sent').limit(rows, (p - 1) * rows).select() |
| page_data = public.get_page(count, p=p, rows=rows, callback='') |
| pattern = r"href='(/v2)?/plugin.*?\?p=(\d+)'" |
| page_data['page'] = re.sub(pattern, r"href='\1'", page_data['page']) |
|
|
| else: |
| |
| wheres = 'task_id=? and ' + fields |
| count = self.M('task_count').where(wheres, (args.task_id, value)).count() |
|
|
| page_data = public.get_page(count, p=p, rows=rows, callback='') |
|
|
| |
| pattern = r"href='(/v2)?/plugin.*?\?p=(\d+)'" |
| |
| page_data['page'] = re.sub(pattern, r"href='\1'", page_data['page']) |
|
|
| try: |
| query = self.M('task_count').where(wheres, (args.task_id, value)) |
| error_list = query.limit(page_data['shift'] + ',' + page_data['row']).select() |
| except: |
| error_list = [] |
| return public.returnMsg(True, {'data': error_list, 'page': page_data['page']}) |
|
|
| def _get_ptr_record(self): |
|
|
| public_ip = self._get_pubilc_ip() |
| if not public_ip: |
| return False |
| try: |
| |
| result = socket.gethostbyaddr(public_ip) |
| if result: |
| if result[0]: |
| return True |
| return False |
| except socket.herror: |
| return False |
|
|
| def _task_mail_send1(self, ): |
|
|
| |
| |
| |
|
|
| cmd = ''' |
| if pgrep -f "send_bulk_script.py" > /dev/null |
| then |
| echo "The task [Sending bulk emails] is executing" |
| exit 1; |
| else |
| btpython /www/server/panel/class/mailModel/script/send_bulk_script.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'] = 'minute-n' |
| data['where1'] = '1' |
| |
| data['sBody'] = cmd |
| data['backupTo'] = '' |
| data['sType'] = 'toShell' |
| data['hour'] = '' |
| data['minute'] = '' |
| 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 _task_mail_send2(self, ): |
|
|
| cmd = ''' |
| if pgrep -f "mail_error_logs.py" > /dev/null |
| then |
| echo "The task [Checking the sent results] is executing" |
| exit 1; |
| else |
| btpython /www/server/panel/class/mailModel/script/mail_error_logs.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'] = 'minute-n' |
| data['where1'] = '1' |
| data['sBody'] = cmd |
| data['backupTo'] = '' |
| data['sType'] = 'toShell' |
| data['hour'] = '' |
| data['minute'] = '' |
| 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 _send_email_all(self, recipients, addresser, password, full_name, subject, content_detail, is_record, task_id): |
|
|
| if recipients['count'] == 0: |
| |
| return |
| |
| send_mail_client = SendMail(addresser, password, 'localhost') |
| |
| send_mail_client.setMailInfo(full_name, subject, content_detail, []) |
|
|
| _, domain = addresser.split('@') |
| sent_recipients_path = f"{self.sent_recipient_path}/toRecipient_{task_id}.log" |
| sent_msgid_path = f"{self.sent_recipient_path}/msgid_{task_id}.log" |
| mail_to = recipients['info'] |
|
|
| orig_content = content_detail |
| proxy_url = self.get_unsubscribe_url() |
|
|
| for user in mail_to: |
| if len(user) == 0: |
| continue |
|
|
| content_detail = orig_content |
|
|
| user_ = [user] |
| |
| msgid = make_msgid() |
|
|
| try: |
| from power_mta.maillog_stat import MailTracker |
| mail_tracker = MailTracker(content_detail, task_id, msgid.strip('<>'), user, proxy_url) |
| mail_tracker.track_links() |
| mail_tracker.append_tracking_pixel() |
| content_detail = mail_tracker.get_html() |
| except: |
| pass |
|
|
| st = send_mail_client.sendMail(user_, domain, is_record, msgid) |
| if not st: |
| |
| send_mail_client = SendMail(addresser, password, 'localhost') |
|
|
| |
| send_mail_client.setMailInfo(full_name, subject, content_detail, []) |
|
|
| |
| public.AppendFile(sent_recipients_path, user + '\n') |
| public.AppendFile(sent_msgid_path, msgid + '\n') |
|
|
|
|
| def get_unsubscribe_url(self,): |
| |
| path = "/www/server/panel/plugin/mail_sys/setinfo.json" |
| url = None |
|
|
| |
| if os.path.exists(path): |
| try: |
| path_info = json.loads(public.readFile(path)) |
| if path_info.get('url'): |
| url = path_info['url'] |
| except json.JSONDecodeError: |
| |
| pass |
|
|
| |
| if not url: |
| ssl_status = public.readFile('/www/server/panel/data/ssl.pl') |
| ssl = 'https' if ssl_status else 'http' |
|
|
| ip = public.readFile("/www/server/panel/data/iplist.txt") |
| port = public.readFile('/www/server/panel/data/port.pl') |
|
|
| |
| if ip and port: |
| url = f"{ssl}://{ip}:{port}" |
|
|
| return url |
|
|
| |
| def _send_email_all_unsubscribe(self, recipients, addresser, password, full_name, subject, content_detail, is_record, task_id, etypes): |
|
|
| if recipients['count'] == 0: |
| |
| return |
|
|
| |
| url = self.get_unsubscribe_url() |
|
|
| |
| send_mail_client = SendMail(addresser, password, 'localhost') |
| |
| |
| |
| send_mail_client.setMailInfo_one(full_name) |
|
|
| _, domain = addresser.split('@') |
| mail_to = recipients['info'] |
| |
| |
| |
| sent_recipients_path = f"{self.sent_recipient_path}/toRecipient_{task_id}.log" |
| sent_msgid_path = f"{self.sent_recipient_path}/msgid_{task_id}.log" |
| |
|
|
| orig_content = content_detail |
|
|
| try: |
| for user in mail_to: |
| if len(user) == 0: |
| |
| continue |
|
|
| content_detail = orig_content |
|
|
| user_ = [user] |
|
|
| |
| msgid = make_msgid() |
|
|
| |
| try: |
| from power_mta.maillog_stat import MailTracker |
| mail_tracker = MailTracker(content_detail, task_id, msgid.strip('<>'), user, url) |
| mail_tracker.track_links() |
| mail_tracker.append_tracking_pixel() |
| content_detail = mail_tracker.get_html() |
| except: |
| pass |
|
|
| |
| |
| mail_jwt = self.generate_jwt(user, etypes, task_id) |
|
|
| |
| url1 = "{}/mailUnsubscribe?action=Unsubscribe&jwt={}".format(url, mail_jwt) |
|
|
| |
| new_content = content_detail.replace('__UNSUBSCRIBE_URL__', url1) |
|
|
| |
| send_mail_client.setMailInfo_two(subject, new_content, []) |
|
|
| st = send_mail_client.sendMail(user_, domain, is_record, msgid) |
|
|
| if not st: |
| |
| send_mail_client = SendMail(addresser, password, 'localhost') |
| send_mail_client.setMailInfo_one(full_name) |
| else: |
| |
| send_mail_client.update_init(full_name) |
|
|
| |
| public.AppendFile(sent_recipients_path, user + '\n') |
| public.AppendFile(sent_msgid_path, msgid + '\n') |
| except: |
| print(public.get_error_info()) |
| public.print_log(public.get_error_info()) |
|
|
| |
| def _get_count_limit(self, domain): |
| key = domain |
|
|
| if domain in ['gmail.com', 'googlemail.com']: |
| key = 'gmail' |
| if domain in ['hotmail.com', 'outlook.com']: |
| key = 'outlook' |
|
|
| |
| count_sent = '/www/server/panel/plugin/mail_sys/count_sent_domain.json' |
| if not os.path.exists(count_sent): |
| data = { |
| 'gmail': 0, |
| 'outlook': 0, |
| 'yahoo.com': 0, |
| 'icloud.com': 0, |
| 'other': 0, |
| } |
| else: |
| try: |
| data = public.readFile(count_sent) |
| data = json.loads(data) |
| except: |
| data = { |
| 'gmail': 0, |
| 'outlook': 0, |
| 'yahoo.com': 0, |
| 'icloud.com': 0, |
| 'other': 0, |
| } |
|
|
| return data[key] |
|
|
| def _mail_error_log_back(self, start, end, error_log, task_id): |
| |
| try: |
|
|
| log_data = public.readFile(self.maillog_path) |
|
|
| |
| status_pattern = r"\bstatus=([a-zA-Z0-9]+)\b" |
|
|
| output_file1 = "/www/server/panel/data/mail/in_bulk/errlog" |
| |
| if not os.path.isdir(output_file1): |
| os.makedirs(output_file1) |
|
|
| |
| with open(error_log, 'w') as f: |
| pass |
|
|
| seen_recipients = set() |
|
|
| with open(error_log, 'a') as f: |
| |
| for line in log_data.splitlines(): |
| err_one = { |
| "task_id": task_id, |
| "recipient": "", |
| "delay": "", |
| "delays": "", |
| "dsn": "", |
| "relay": "", |
| "domain": "", |
| "status": "", |
| "err_info": "", |
| } |
|
|
| try: |
|
|
| try: |
| |
| log_time = parse(line[:31]) |
| |
|
|
| except ValueError: |
| |
| timestamp_str = line[:15] |
| try: |
| current_year = datetime.now().year |
| |
| timestamp_str = f"{timestamp_str} {current_year}" |
| log_time = datetime.strptime(timestamp_str, '%b %d %H:%M:%S %Y') |
| except ValueError: |
| |
|
|
| |
| log_time = datetime.now() |
|
|
| |
| log_time = int(log_time.timestamp()) |
| |
|
|
| if end >= log_time >= start: |
| match = re.search(status_pattern, line) |
| if match and (match.group(1) != "sent"): |
| |
| |
| match1 = re.search(r'to=<([^>]+)>', line) |
| if match1: |
| recipient = match1.group(1) |
| |
| match2 = re.search(r'status=([^ ]+)', line) |
| if match2: |
| status = match2.group(1) |
| |
| match3 = re.search(r'\((.*?)\)', line) |
| if match3: |
| err_info = match3.group(1) |
| |
| match4 = re.search(r'delay=(\d+(\.\d+)?)', line) |
| if match4: |
| delay = match4.group(1) |
| |
| match5 = re.search(r'delays=([\d./*]+)', line) |
| if match5: |
| delays = match5.group(1) |
| |
| match6 = re.search(r'dsn=([\d.]+)', line) |
| if match6: |
| dsn = match6.group(1) |
| |
| match7 = re.search(r'relay=(.*?)(?=,| )', line) |
| if match7: |
| relay = match7.group(1) |
|
|
| name, domain = recipient.split('@') |
| if name == 'postmaster': |
| continue |
|
|
| else: |
| |
| |
| err_one['recipient'] = recipient |
| err_one['domain'] = domain |
| err_one['status'] = status |
| err_one['delay'] = delay |
| err_one['delays'] = delays |
| err_one['dsn'] = dsn |
| err_one['relay'] = relay |
| err_one['err_info'] = err_info |
| if recipient not in seen_recipients: |
| seen_recipients.add(recipient) |
| f.write(line + '\n') |
| self.M('task_count').insert(err_one) |
|
|
| except ValueError: |
| print(public.get_error_info()) |
| public.print_log(public.get_error_info()) |
| pass |
| return True |
| except Exception as e: |
| print(public.get_error_info()) |
| public.print_log(public.get_error_info()) |
| return False |
|
|
| |
| def get_message_ids_from_task_file(self, task_id): |
| """获取群发任务的message_ids""" |
|
|
| message_ids = set() |
| task_file_path = f"{self.sent_recipient_path}/msgid_{task_id}.log" |
| |
| if os.path.exists(task_file_path): |
| data = public.readFile(task_file_path) |
| for line in data.splitlines(): |
| msgid = line.strip() |
| msgid = msgid.strip('<>') |
| |
| message_ids.add(msgid) |
| |
| |
| message_ids = list(message_ids) |
| return message_ids |
|
|
| def parse_log_time(self, line): |
| """日志时间转普通时间戳""" |
| try: |
| |
| if line[:4].isdigit(): |
| return int(parse(line[:31]).timestamp()) |
|
|
| |
| current_year = datetime.now().year |
| timestamp_str = f"{line[:15]} {current_year}" |
| return int(datetime.strptime(timestamp_str, '%b %d %H:%M:%S %Y').timestamp()) |
| except: |
| return int(datetime.now().timestamp()) |
|
|
|
|
| def _mail_error_log(self,error_log, task_id): |
| """分析日志并记录到群发独立数据库""" |
| database_path = f'/www/vmail/bulk/task_{task_id}.db' |
|
|
| try: |
|
|
| output_file1 = "/www/server/panel/data/mail/in_bulk/errlog" |
| if not os.path.isdir(output_file1): |
| os.makedirs(output_file1) |
|
|
| seen_recipients = set() |
|
|
| |
| message_ids = self.get_message_ids_from_task_file(task_id) |
| if not message_ids: |
| print('Message id is empty, skip') |
| |
| return |
| |
| |
| today_ = datetime.now() |
| today = today_.strftime('%b ') + (str(today_.day).rjust(2) if today_.day < 10 else str(today_.day)) |
| today0 = datetime.now().strftime('%b %-d') |
|
|
| |
| full_date = today_.strftime('%Y-%m-%d') |
|
|
| |
| cmd = f"grep -E '({full_date}|{today})' {self.maillog_path} > /tmp/recent_mass_posting.log" |
| if today0 != today: |
| cmd = f"grep -E '({full_date}|{today}|{today0})' {self.maillog_path} > /tmp/recent_mass_posting.log" |
|
|
| public.ExecShell(cmd) |
|
|
| |
| |
| log_data = public.readFile('/tmp/recent_mass_posting.log') |
|
|
| try: |
| |
| queue_id_to_message_id = {} |
| |
| queue_id_to_status_info = {} |
| |
| queue_id_to_line = {} |
|
|
| |
| aamsh = [] |
|
|
| |
| message_id_pattern = re.compile(r'message-id=<([^>]+)>') |
| queue_id_pattern = re.compile(r'postfix/\S+\[\d+\]: (\w+):') |
| status_pattern = re.compile(r'status=([^ ]+)') |
| recipient_pattern = re.compile(r'to=<([^>]+)>') |
| status_code_pattern = re.compile(r'\b(\d+)\s(\d+\.\d+\.\d+)\b') |
|
|
| delay_pattern = re.compile(r'delay=(\d+(\.\d+)?)') |
| delays_pattern = re.compile(r'delays=([\d./*]+)') |
| dsn_pattern = re.compile(r'dsn=([\d.]+)') |
| relay_pattern = re.compile(r'relay=(.*?)(?=,| )') |
| err_info_pattern = re.compile(r'\((.*?)\)') |
|
|
| for line in log_data.splitlines(): |
| log_time = self.parse_log_time(line) |
|
|
| message_id_match = message_id_pattern.search(line) |
| |
| if message_id_match: |
| message_id = message_id_match.group(1) |
| aamsh.append(message_id) |
| if message_id in message_ids: |
| queue_id_match = queue_id_pattern.search(line) |
| if queue_id_match: |
| queue_id = queue_id_match.group(1) |
| queue_id_to_message_id[queue_id] = message_id |
|
|
| |
| status_match = status_pattern.search(line) |
| recipient_match = recipient_pattern.search(line) |
| if status_match and recipient_match: |
|
|
| if 'postmaster@' in recipient_match.group(1): |
| |
| continue |
| queue_id_match = queue_id_pattern.search(line) |
| if queue_id_match: |
| queue_id = queue_id_match.group(1) |
| |
|
|
| queue_id_to_status_info[queue_id] = { |
| 'recipient': recipient_match.group(1), |
| 'status': status_match.group(1), |
| 'delay': delay_pattern.search(line).group(1) if delay_pattern.search(line) else '', |
| 'delays': delays_pattern.search(line).group(1) if delays_pattern.search(line) else '', |
| 'dsn': dsn_pattern.search(line).group(1) if dsn_pattern.search(line) else '', |
| 'relay': relay_pattern.search(line).group(1) if relay_pattern.search(line) else '', |
| 'err_info': err_info_pattern.search(line).group(1) if err_info_pattern.search( |
| line) else '', |
| 'created': log_time, |
| } |
|
|
|
|
| status_code_match = status_code_pattern.search(queue_id_to_status_info[queue_id]['err_info']) |
|
|
| if status_code_match: |
| status_code = status_code_match.group(1) |
| else: |
| status_code = 101 |
|
|
| queue_id_to_status_info[queue_id]['code'] = int(status_code) |
|
|
| queue_id_to_line[queue_id] = line |
|
|
| except Exception as e: |
| print(public.get_error_info()) |
| public.print_log(public.get_error_info()) |
|
|
| return False |
|
|
| |
| insert_data = [] |
| with open(error_log, 'a') as f: |
| for queue_id, message_id in queue_id_to_message_id.items(): |
| if queue_id in queue_id_to_status_info: |
| status_info = queue_id_to_status_info[queue_id] |
| |
| |
|
|
| if status_info['recipient'] in seen_recipients: |
| continue |
|
|
| seen_recipients.add(status_info['recipient']) |
| |
| if queue_id_to_line[queue_id]: |
| f.write(f"{queue_id_to_line[queue_id]}\n") |
|
|
| |
| err_data = { |
| 'recipient': status_info['recipient'], |
| 'domain': status_info['recipient'].split('@')[1], |
| 'status': status_info['status'], |
| 'delay': status_info['delay'], |
| 'delays': status_info['delays'], |
| 'dsn': status_info['dsn'], |
| 'relay': status_info['relay'], |
| 'err_info': status_info['err_info'], |
| 'created': status_info['created'], |
| 'queue_id': queue_id, |
| 'message_id': message_id, |
| 'code': status_info['code'], |
| } |
| insert_data.append(err_data) |
|
|
| if len(insert_data) >= 5000: |
| with public.S("task_count", database_path) as obj: |
| aa = obj.insert_all(insert_data, option='IGNORE') |
|
|
| insert_data = [] |
|
|
| if len(insert_data) > 0: |
|
|
| with public.S("task_count", database_path) as obj: |
| aa = obj.insert_all(insert_data, option='IGNORE') |
| return True |
| except Exception as e: |
| print(public.get_error_info()) |
| public.print_log(public.get_error_info()) |
|
|
| return False |
|
|
| 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 dns.resolver.NoAnswer: |
| 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 check_ptr_domain(self, domain): |
| ''' |
| 查询域名和ip 用于安装webmail |
| :param args: |
| :return: |
| ''' |
| key = '{0}:{1}'.format(domain, 'PTR') |
| session = public.readFile('/www/server/panel/plugin/mail_sys/session.json') |
| if session: |
| session = json.loads(session) |
| else: |
| session = {} |
| isptr = session[key]['status'] |
| return isptr |
|
|
| def get_SECRET_KEY(self): |
| path = '/www/server/panel/data/mail/jwt-secret.txt' |
| if not os.path.exists(path): |
| secretKey = public.GetRandomString(64) |
| public.writeFile(path, secretKey) |
| secretKey = public.readFile(path) |
| return secretKey |
|
|
| def generate_jwt(self, email, etypes, task_id): |
| |
| SECRET_KEY = self.get_SECRET_KEY() |
| payload = { |
| 'email': email, |
| |
| 'etype': etypes, |
| 'task_id': task_id, |
| 'exp': datetime.utcnow() + timedelta(days=7) |
| } |
|
|
| token = jwt.encode(payload, SECRET_KEY, algorithm='HS256') |
| return token |
|
|
| def update_task(self, args): |
| |
|
|
| try: |
| |
| if not hasattr(args, 'id') or args.get('id/d', 0) == 0: |
| return public.returnMsg(False, public.lang('参数错误: id')) |
| task_old = self.M('email_task').where('id=?', args.get('id/d', 0)).find() |
| if not isinstance(task_old, dict): |
| return public.returnMsg(False, public.lang('Task does not exist')) |
| if not hasattr(args, 'temp_id') or args.get('temp_id/d', 0) == 0: |
| return public.returnMsg(False, public.lang('参数错误: temp_id')) |
| temp_id = args.temp_id |
|
|
| if not hasattr(args, 'addresser') or args.get('addresser/s', '') == '': |
| return public.returnMsg(False, public.lang('参数错误: addresser')) |
| if not hasattr(args, 'task_name') or args.get('task_name/s', '') == '': |
| return public.returnMsg(False, public.lang('参数错误: task_name')) |
|
|
| task_name = args.get('task_name', '') |
|
|
| |
| if not hasattr(args, 'etypes') or args.get('etypes', '') == '': |
| etypes = '1' |
| else: |
| etypes = args.etypes |
|
|
| etype_list = etypes.split(',') |
|
|
| |
| with public.S("mail_unsubscribe", '/www/vmail/mail_unsubscribe.db') as obj: |
| count = obj.where_in('etype', etype_list).where('active', 1).count() |
| if not count: |
| return public.returnMsg(False, 'The selected contact list is empty') |
| namemd5 = public.md5(task_name) |
| |
| |
| recipient_path = "{}/recipient/{}_verify_{}".format(self.in_bulk_path, namemd5, etypes) |
| |
| recipient_count, black_num, abnormal_num = self.processing_recipient_v2(recipient_path, etype_list) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| addresser = args.get('addresser', '') |
| old_task_id = args.get('id', 0) |
|
|
| |
| is_record = args.get('is_record/d', 0) |
| is_record = 1 if is_record else 0 |
|
|
| |
| unsubscribe = args.get('unsubscribe/d', 0) |
| unsubscribe = 1 if unsubscribe else 0 |
|
|
| |
| threads = args.get('threads/d', 0) |
| threads = int(threads) if threads else 0 |
|
|
| |
| if not hasattr(args, 'pause') or args.get('pause/d', 0) == 0: |
| pause = 0 |
| else: |
| pause = 1 |
| |
| if not hasattr(args, 'full_name') or args.get('full_name', '') == '': |
| full_name = '' |
| else: |
| full_name = args.get('full_name', '') |
| |
| remark = args.get('remark', '') |
|
|
| |
| subject = args.get('subject', '') |
|
|
| |
| if not hasattr(args, 'start_time') or args.get('start_time/d', 0) == 0: |
| start_time = int(time.time()) |
| else: |
| start_time = int(args.start_time) |
|
|
| task_process = 0 |
|
|
| self.init_send() |
| |
|
|
| |
| args1 = public.dict_obj() |
| args1.task_id = old_task_id |
| dfg = self.delete_task(args1) |
|
|
|
|
| |
| timestamp = int(time.time()) |
| task_id = 0 |
| try: |
| |
| task_id = self.M('email_task').add( |
| 'task_name,addresser,recipient_count,task_process,pause,temp_id,is_record,unsubscribe,threads,created,modified,start_time,remark,etypes,recipient,subject,full_name', |
| (task_name, addresser, recipient_count, task_process, pause, temp_id, is_record, unsubscribe, threads, |
| timestamp, timestamp, start_time, remark, etypes, recipient_path, subject,full_name)) |
|
|
| |
| error_log = "/www/server/panel/data/mail/in_bulk/errlog/task_{}.log".format(task_id) |
| if not os.path.exists(error_log): |
| public.WriteFile(error_log, '') |
| |
| self.create_task_database(task_id) |
| |
| self._task_mail_send1() |
| self._task_mail_send2() |
| |
| |
|
|
|
|
| return public.returnMsg(True, public.lang('任务添加成功')) |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
| |
| |
| self.M('email_task').where('id=?', task_id).delete() |
| return public.returnMsg(False, public.lang('任务添加失败: [{}]', str(e))) |
|
|
|
|
| except Exception as e: |
|
|
| public.print_log(public.get_error_info()) |
|
|
| |
| def init_send(self): |
| |
| |
| |
|
|
| |
| start_mark = '/www/server/panel/plugin/mail_sys/start_Task.pl' |
| start_send = '/www/server/panel/plugin/mail_sys/start_Send.pl' |
| |
| if os.path.exists(start_mark): |
| os.remove(start_mark) |
| if os.path.exists(start_send): |
| os.remove(start_send) |
| |
| |
|
|
| |
| |
| |
|
|
| |
| def recipient_blacklist(self): |
|
|
| |
| if not self._recipient_blacklist_status(): |
| |
| return [] |
|
|
| postfix_recipient_blacklist = '/etc/postfix/blacklist' |
| |
| if not os.path.exists(postfix_recipient_blacklist): |
| return [] |
|
|
| try: |
| with open(postfix_recipient_blacklist, 'r') as file: |
| emails = file.read().splitlines() |
| except Exception as e: |
| return [] |
|
|
| |
| if emails: |
| emails = [email.split()[0] for email in emails] |
| else: |
| return [] |
|
|
| return emails |
|
|
| def _recipient_blacklist_status(self): |
| |
| postfix_main_cf = "/etc/postfix/main.cf" |
| result = public.readFile(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 _get_user_free_quota(self): |
| """获取当月免费额度 免费版2w 专业版12w""" |
| endtime = public.get_pd()[1] |
| curtime = int(time.time()) |
|
|
| quota = 20000 |
| |
| if endtime == 0 or endtime > curtime: |
| quota = 120000 |
| |
| |
| return quota |
|
|
| def _get_user_pack_quota(self): |
| """获取补充包信息""" |
| data = { |
| 'total': 0, |
| 'used': 0, |
| 'available': 0, |
| 'packages': [], |
| } |
|
|
| from panelPlugin import panelPlugin |
| pp = panelPlugin() |
| a = public.to_dict_obj({}) |
| |
|
|
| try: |
| softList = pp.get_soft_list(a) |
| except: |
| softList = {} |
|
|
| |
| if softList.get('expansions', None): |
| mail = softList['expansions']['mail'] |
|
|
| data['total'] = mail.get('total', 0) |
| data['used'] = mail.get('used', 0) |
| data['available'] = mail.get('available', 0) |
| data['packages'] = mail.get('packages', []) |
|
|
| return data |
|
|
| |
| def _get_user_quota(self): |
| """用户当月可用额度 免费剩余+付费包剩余""" |
| |
| quota = self._get_user_free_quota() |
|
|
| |
| senduse = self._get_month_senduse() |
|
|
| |
| pack = self._get_user_pack_quota() |
|
|
| |
| |
| packnum = pack['available'] |
| free = quota-senduse if quota > senduse else 0 |
|
|
| user_quota = free + packnum |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| return user_quota |
|
|
| |
| def _get_month_senduse(self): |
| """获取本月发件数 本地和线上比较取最大 本地改数据库+当天""" |
| |
| dnum = self.get_data_month_count(None) |
| todaynum = self.get_today_sendnum() |
|
|
| pack = self._get_user_pack_quota() |
| quota = self._get_user_free_quota() |
| pack_use = pack['used'] |
| cnum = 0 |
| if pack_use > 0: |
| cnum = pack_use + quota |
|
|
| senduse = dnum + todaynum |
|
|
| |
| if senduse < cnum: |
| return cnum |
| else: |
| return senduse |
|
|
| def get_today_sendnum(self): |
|
|
| |
| cache_key = 'mail_sys:get_today_sendnum' |
| cache = public.cache_get(cache_key) |
|
|
| if cache: |
| return cache |
| output, err = public.ExecShell( |
| f'pflogsumm -d today --verbose-msg-detail --zero-fill --iso-date-time --rej-add-from {self.maillog_path}') |
|
|
| |
| data = self._pflogsumm_data_treating(output) |
|
|
| |
| all = 0 |
| if data.get('stats_dict', None): |
| all = data['stats_dict'].get('delivered', 0) |
|
|
| |
| public.cache_set(cache_key, all, 15) |
| return all |
|
|
| def get_yesterday_count(self): |
| """ 昨日数据插入概览统计表 """ |
|
|
| |
| cache_key = 'mail_sys:get_yesterday_count' |
| cache = public.cache_get(cache_key) |
|
|
| if cache: |
| return |
|
|
| |
| yesterday = datetime.now() - timedelta(days=1) |
| yesterday_midnight = datetime(yesterday.year, yesterday.month, yesterday.day) |
| yesterday_0 = int(time.mktime(yesterday_midnight.timetuple())) |
| yesterday_24 = yesterday_0 + 86400 |
|
|
| with self.M("log_analysis") as obj: |
| query = obj.where('time >=? and time <?', (yesterday_0, yesterday_24)).count() |
| |
| if query == 24: |
| |
| return |
|
|
|
|
| output, err = public.ExecShell( |
| f'pflogsumm -d yesterday --verbose-msg-detail --zero-fill --iso-date-time --rej-add-from {self.maillog_path}') |
|
|
| |
| data = self._pflogsumm_data_treating(output, day='yesterday') |
|
|
| |
| |
| if data.get('hourly_stats', None) and len(data['hourly_stats']) == 24: |
| |
|
|
|
|
| try: |
| with public.S("log_analysis", "/www/vmail/postfixadmin.db") as obj: |
| aa = obj.insert_all(data['hourly_stats'], option='IGNORE') |
| except: |
| public.print_log(public.get_error_info()) |
|
|
| |
| |
| |
| |
| |
| public.cache_set(cache_key, 1, 3600) |
| return |
|
|
| def upload_yesterday_email_usage(self, output): |
| """ 上传补充包使用量 """ |
|
|
| |
| |
| quota = self._get_user_free_quota() |
| |
| senduse = self._get_month_senduse() |
| |
| upload = False if quota > senduse else True |
| if not upload: |
| |
| return False |
|
|
| |
| data = self._pflogsumm_data_treating(output, timezone='utc') |
|
|
| if not isinstance(data['hourly_stats'], list): |
| |
| return False |
| data = data['hourly_stats'] |
| if len(data) != 24: |
| |
| return False |
|
|
| |
| submat_data = [] |
| for i in data: |
| all = i['delivered'] |
| if all == 0: |
| continue |
| submat_data.append({ |
| "day_time_utc": i['time'], |
| "used": all |
| }) |
| if not submat_data: |
| |
| return False |
|
|
| import panelAuth |
| import requests |
| from BTPanel import session |
| cloudUrl = '{}/api/panel/submit_expand_pack_used'.format(public.OfficialApiBase()) |
| pdata = panelAuth.panelAuth().create_serverid(None) |
| url_headers = {} |
| if 'token' in pdata: |
| url_headers = {"authorization": "bt {}".format(pdata['token'])} |
|
|
| pdata['environment_info'] = json.dumps(public.fetch_env_info()) |
| pdata['data'] = submat_data |
| pdata['expand_pack_type'] = "mail" |
|
|
| listTmp = requests.post(cloudUrl, json=pdata, headers=url_headers) |
| ret = listTmp.json() |
|
|
| if not ret['success']: |
| print(f"|-{ret['res']}") |
| return ret['res'] |
| else: |
| |
| public.load_soft_list() |
| public.refresh_pd() |
|
|
| |
| def get_data_month_count(self, args): |
| """数据库 获取本月发件数""" |
| |
| |
| |
| |
| |
| |
|
|
| |
| cache_key = 'mail_sys:get_data_month_count' |
| cache = public.cache_get(cache_key) |
|
|
| if cache: |
| return cache |
|
|
|
|
| |
| now = datetime.now() |
| first_day_of_month = datetime(now.year, now.month, 1) |
| timestamp_first_day = int(time.mktime(first_day_of_month.timetuple())) |
|
|
| |
| timestamp_now = int(time.time()) |
| 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" |
| query = self.M('log_analysis').field(total_fields).where('time between ? and ?', |
| (timestamp_first_day, timestamp_now)).find() |
|
|
| if isinstance(query, str): |
| |
| return 0 |
|
|
| sentall = query['sentall'] |
|
|
| public.cache_set(cache_key, sentall, 15) |
| return sentall |
| except: |
| public.print_log(public.get_error_info()) |
| return 0 |
|
|
| |
| def get_pflogsumm_month_count(self, args): |
| """命令 获取本月发件数""" |
| |
| cache_key = 'mail_sys:get_pflogsumm_month_count' |
| cache = public.cache_get(cache_key) |
|
|
| if cache: |
| return cache |
| log_data = public.readFile(self.maillog_path) |
| if not log_data: |
| |
| return 0 |
|
|
| |
| current_time = time.localtime() |
| current_year = time.strftime("%Y", current_time) |
| current_month_num = time.strftime("%m", current_time) |
| current_month_abbr = time.strftime("%b", current_time) |
|
|
| |
| first_line = log_data.splitlines()[0] if log_data else None |
|
|
| if first_line is None: |
| |
| return 0 |
|
|
| try: |
|
|
| |
| if first_line[:2].isdigit(): |
| |
| date = f"{current_year}-{current_month_num}" |
| public.ExecShell(f'grep "{date}" {self.maillog_path} > /tmp/mail_filtered.log') |
| else: |
| |
| public.ExecShell(f'grep "{current_month_abbr}" {self.maillog_path} > /tmp/mail_filtered.log') |
|
|
| output, err = public.ExecShell('pflogsumm /tmp/mail_filtered.log') |
| |
| |
|
|
| res = self.parse_pflogsumm_grand_totals(output) |
|
|
| stats_dict = res['stats_dict'] |
| |
|
|
| sentall = stats_dict.get('delivered', 0) + stats_dict.get('bounced', 0) + stats_dict.get('rejected', 0) |
|
|
| public.cache_set(cache_key, sentall, 15) |
| return sentall |
|
|
| except Exception as e: |
| public.print_log(f"get_pflogsumm_month_count: {e}") |
|
|
| |
| def _pflogsumm_data_treating(self, output, timezone=None, day='today'): |
| """ 处理命令返回数据 -- 当天/昨天""" |
| stats_dict = {} |
|
|
| |
| patterns = [ |
| r'(\d+)\s+received', |
| r'(\d+)\s+delivered', |
| r'(\d+)\s+forwarded', |
| r'(\d+)\s+deferred\s+\((\d+)\s+deferrals\)', |
| r'(\d+)\s+bounced', |
| r'(\d+)\s+rejected\s+\((\d+)%\)', |
| r'(\d+)\s+reject\s+warnings', |
| r'(\d+)\s+held', |
| r'(\d+)\s+discarded\s+\((\d+)%\)', |
| r'(\d+)\s+bytes\s+received', |
| r'(\d+)k\s+bytes\s+delivered', |
| r'(\d+)\s+senders', |
| r'(\d+)\s+sending\s+hosts/domains', |
| r'(\d+)\s+recipients', |
| r'(\d+)\s+recipient\s+hosts/domains' |
| ] |
|
|
| for pattern in patterns: |
| match = re.search(pattern, output) |
| if match: |
| |
| stats_dict[pattern] = int(match.group(1)) |
|
|
| friendly_names = { |
| r'(\d+)\s+received': 'received', |
| r'(\d+)\s+delivered': 'delivered', |
| r'(\d+)\s+forwarded': 'forwarded', |
| r'(\d+)\s+deferred\s+\((\d+)\s+deferrals\)': 'deferred', |
| r'(\d+)\s+bounced': 'bounced', |
| r'(\d+)\s+rejected\s+\((\d+)%\)': 'rejected', |
| r'(\d+)\s+reject\s+warnings': 'reject_warnings', |
| r'(\d+)\s+held': 'held', |
| r'(\d+)\s+discarded\s+\((\d+)%\)': 'discarded', |
| r'(\d+)\s+bytes\s+received': 'bytes_received', |
| r'(\d+)k\s+bytes\s+delivered': 'bytes_delivered_kilo', |
| r'(\d+)\s+senders': 'senders', |
| r'(\d+)\s+sending\s+hosts/domains': 'sending_hosts_domains', |
| r'(\d+)\s+recipients': 'recipients', |
| r'(\d+)\s+recipient\s+hosts/domains': 'recipient_hosts_domains' |
| } |
|
|
| stats_dict = {friendly_names[key]: value for key, value in stats_dict.items() if key in friendly_names} |
| keys_to_remove = [ |
| "reject_warnings", |
| "held", |
| "discarded", |
| "bytes_received", |
| "senders", |
| "sending_hosts_domains", |
| "recipients", |
| "recipient_hosts_domains" |
| ] |
|
|
| for key in keys_to_remove: |
| stats_dict.pop(key, None) |
|
|
| |
| pattern = r'(\d{2}:\d{2}-\d{2}:\d{2})\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)' |
| hourly_stats_list = [] |
| matches = re.findall(pattern, output) |
|
|
| |
| for match in matches: |
| hour = match[0] |
| received = int(match[1]) |
| delivered = int(match[2]) |
| deferred = int(match[3]) |
| bounced = int(match[4]) |
| rejected = int(match[5]) |
| |
|
|
| if timezone == 'utc': |
| hourly_stats_obj = { |
| "time": self._str_to_tstp_utc(hour), |
| 'received': received, |
| 'delivered': delivered, |
| 'deferred': deferred, |
| 'bounced': bounced, |
| 'rejected': rejected, |
| } |
| else: |
| hourly_stats_obj = { |
| "time": self._str_to_tstp(hour), |
| 'received': received, |
| 'delivered': delivered, |
| 'deferred': deferred, |
| 'bounced': bounced, |
| 'rejected': rejected, |
| } |
| if day == 'yesterday': |
| hourly_stats_obj['time'] = hourly_stats_obj['time']-86400 |
|
|
| hourly_stats_list.append(hourly_stats_obj) |
|
|
|
|
| data = { |
| "hourly_stats": hourly_stats_list, |
| "stats_dict": stats_dict, |
| } |
| return data |
|
|
| def parse_pflogsumm_grand_totals(self, output): |
| """ 处理命令返回数据 """ |
| stats_dict = {} |
|
|
| patterns = [ |
| r'(\d+)\s+received', |
| r'(\d+)\s+delivered', |
| r'(\d+)\s+forwarded', |
| r'(\d+)\s+deferred\s+\((\d+)\s+deferrals\)', |
| r'(\d+)\s+bounced', |
| r'(\d+)\s+rejected\s+\((\d+)%\)', |
| r'(\d+)\s+reject\s+warnings', |
| r'(\d+)\s+held', |
| r'(\d+)\s+discarded\s+\((\d+)%\)', |
| r'(\d+)\s+bytes\s+received', |
| r'(\d+)k\s+bytes\s+delivered', |
| r'(\d+)\s+senders', |
| r'(\d+)\s+sending\s+hosts/domains', |
| r'(\d+)\s+recipients', |
| r'(\d+)\s+recipient\s+hosts/domains' |
| ] |
|
|
| for pattern in patterns: |
| match = re.search(pattern, output) |
| if match: |
| stats_dict[pattern] = int(match.group(1)) |
| |
| friendly_names = { |
| r'(\d+)\s+received': 'received', |
| r'(\d+)\s+delivered': 'delivered', |
| r'(\d+)\s+forwarded': 'forwarded', |
| r'(\d+)\s+deferred\s+\((\d+)\s+deferrals\)': 'deferred', |
| r'(\d+)\s+bounced': 'bounced', |
| r'(\d+)\s+rejected\s+\((\d+)%\)': 'rejected', |
| r'(\d+)\s+reject\s+warnings': 'reject_warnings', |
| r'(\d+)\s+held': 'held', |
| r'(\d+)\s+discarded\s+\((\d+)%\)': 'discarded', |
| r'(\d+)\s+bytes\s+received': 'bytes_received', |
| r'(\d+)k\s+bytes\s+delivered': 'bytes_delivered_kilo', |
| r'(\d+)\s+senders': 'senders', |
| r'(\d+)\s+sending\s+hosts/domains': 'sending_hosts_domains', |
| r'(\d+)\s+recipients': 'recipients', |
| r'(\d+)\s+recipient\s+hosts/domains': 'recipient_hosts_domains' |
| } |
|
|
| stats_dict = {friendly_names[key]: value for key, value in stats_dict.items() if key in friendly_names} |
| |
| data = {"stats_dict": stats_dict} |
| return data |
| |
| def _get_asd(self): |
| """获取系统时间与utc的差值""" |
| |
| current_local_time = datetime.now() |
| current_local_timestamp = int(current_local_time.timestamp()) |
|
|
| |
| current_utc_time = datetime.utcnow() |
| current_utc_timestamp = int(current_utc_time.timestamp()) |
|
|
| |
| timezone_offset = current_local_timestamp - current_utc_timestamp |
|
|
| if timezone_offset > 0: |
| return timezone_offset, True |
| else: |
| return abs(timezone_offset), False |
|
|
| def _str_to_tstp_utc(self, start_time): |
| """00:00-01:00改为utc时间戳""" |
| |
| current_date = datetime.now().date() |
| start_time = start_time.split('-')[0] |
| |
| combined_datetime_str = f"{current_date} {start_time}" |
| combined_datetime = datetime.strptime(combined_datetime_str, |
| "%Y-%m-%d %H:%M") |
|
|
| unix_timestamp = int(combined_datetime.timestamp()) |
| h, e = self._get_asd() |
| if e: |
| unix_timestamp = unix_timestamp - h |
| else: |
|
|
| unix_timestamp = unix_timestamp + h |
| return unix_timestamp |
|
|
| def _str_to_tstp(self, start_time): |
| """00:00-01:00改为时间戳""" |
| |
| current_date = datetime.now().date() |
| start_time = start_time.split('-')[0] |
| |
| combined_datetime_str = f"{current_date} {start_time}" |
| combined_datetime = datetime.strptime(combined_datetime_str, "%Y-%m-%d %H:%M") |
| |
| unix_timestamp = int(combined_datetime.timestamp()) |
| return unix_timestamp |
|
|
| |
| |
| def get_quota_occupation(self): |
| ''' |
| 统计额度占用 |
| :param args: |
| :return: |
| ''' |
| |
| path = '/www/server/panel/plugin/mail_sys/data/quota_occupation.json' |
|
|
| if os.path.exists(path): |
| data = public.readFile(path) |
| try: |
| data = json.loads(data) |
| except: |
| pass |
| else: |
| data = {} |
| occupation = 0 |
| if data: |
| occupation = sum(data.values()) |
| res = { |
| "occupation": occupation, |
| "info": data |
| } |
| return res |
|
|
| |
| def add_quota_occupation(self, task_id, occupation): |
| ''' |
| 新增 任务占用额度 |
| :param args: task_id str 任务id |
| :param args: occupation int 占用数量 |
| :return: |
| ''' |
|
|
| |
| path = '/www/server/panel/plugin/mail_sys/data/quota_occupation.json' |
| data = {} |
| if os.path.exists(path): |
| data = public.readFile(path) |
| try: |
| data = json.loads(data) |
| except: |
| pass |
| data[task_id] = int(occupation) |
| public.writeFile(path, public.GetJson(data)) |
|
|
| return True |
|
|
| |
| def del_quota_occupation(self, task_id): |
| ''' |
| 删除指定任务占用的额度 |
| :param args: task_id str 任务id |
| :return: bool |
| ''' |
| |
| path = '/www/server/panel/plugin/mail_sys/data/quota_occupation.json' |
|
|
| if os.path.exists(path): |
| data = public.readFile(path) |
| try: |
| data = json.loads(data) |
| except: |
| pass |
| |
| if task_id in data: |
| del data[task_id] |
| else: |
| return False |
| else: |
| return False |
|
|
| public.writeFile(path, public.GetJson(data)) |
|
|
| return True |
|
|
| |
| def Ms(self, table_name, db_path): |
| import db |
| sql = db.Sql() |
| sql._Sql__DB_FILE = db_path |
| sql._Sql__encrypt_keys = [] |
| return sql.table(table_name) |
|
|
| def create_task_database(self, taskid): |
| """生成群发任务的数据库""" |
| db_dir = '/www/vmail/bulk' |
| db_path = f'{db_dir}/task_{taskid}.db' |
| if not os.path.exists(db_dir): |
| os.makedirs(db_dir) |
| os.system('chown -R vmail:mail /www/vmail/bulk') |
| |
| |
| sql = '''CREATE TABLE IF NOT EXISTS `task_count` ( |
| `id` INTEGER PRIMARY KEY AUTOINCREMENT, |
| `queue_id` varchar(320) NOT NULL, -- 邮件队列id |
| `message_id` TEXT NOT NULL, -- 邮件 message_id |
| `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, -- 状态 |
| `code` INTEGER, -- 状态码 250 5xx 101 |
| `err_info` text NOT NULL, -- 详情 |
| UNIQUE(message_id,recipient) |
| );''' |
|
|
| with self.Ms("", db_path) as obj: |
| aa = obj.execute(sql, ()) |
|
|
|
|
| def get_abnormal_recipient(self): |
| """获取异常邮件 拒绝状态 + 延迟3次""" |
| abnormal_list = [] |
| with public.S("abnormal_recipient", '/www/vmail/abnormal_recipient.db') as obj1: |
| abnormal = obj1.where('count >=? OR status =?', (3, 'bounced')).select() |
| if abnormal: |
| abnormal_list = [i['recipient'] for i in abnormal] |
| return abnormal_list |
|
|
| |
| def get_send_progress(self, task_id): |
| """ |
| 获取邮件发送进度。 |
| |
| :param task_id: 任务id |
| :return: 发送进度(百分比) |
| """ |
| database_path = f'/www/vmail/bulk/task_{task_id}.db' |
| with public.S("task_count", database_path) as obj: |
| total_sent = obj.count() |
| return total_sent |
|
|
| |
| def _check_sender_domain_restrictions(self): |
| """ 是否限制域名发件 False未限制 True限制""" |
| |
| result = public.readFile(self.postfix_main_cf) |
|
|
| match = re.search(r"smtpd_sender_restrictions\s*=\s*(.+)", result) |
| if not match: |
| return False |
|
|
| restrictions = match.group(1) |
| if 'check_sender_access hash:/etc/postfix/sender_black' not in restrictions: |
| return False |
| else: |
| return True |
|
|
| |
| def _domain_restrictions_switch(self, status): |
| """ 域名限制 开关 开启 Ture, 关闭 False""" |
| |
| try: |
| |
| result = public.readFile(self.postfix_main_cf) |
| if not result: |
| return False |
|
|
| |
| blacklist_entry_parts = ['check_sender_access', 'hash:/etc/postfix/sender_black'] |
| blacklist_entry_str = ' '.join(blacklist_entry_parts) |
|
|
| |
| match = re.search(r"smtpd_sender_restrictions\s*=\s*(.+)", result, re.IGNORECASE) |
|
|
| if status: |
| |
| if not match: |
| updated_config = f"{result}\nsmtpd_sender_restrictions = {blacklist_entry_str}\n" |
| public.writeFile(self.postfix_main_cf, updated_config) |
| |
| else: |
| current_restrictions = match.group(1).split() |
| |
| if not all(part in current_restrictions for part in blacklist_entry_parts): |
| current_restrictions.extend(blacklist_entry_parts) |
| updated_config = re.sub( |
| r"smtpd_sender_restrictions\s*=.*", |
| f"smtpd_sender_restrictions = {' '.join(current_restrictions)}", |
| result, |
| flags=re.IGNORECASE |
| ) |
| public.writeFile(self.postfix_main_cf, updated_config) |
| |
|
|
| else: |
| |
| if match: |
| current_restrictions = match.group(1).split() |
| |
| if all(part in current_restrictions for part in blacklist_entry_parts): |
| for part in blacklist_entry_parts: |
| while part in current_restrictions: |
| current_restrictions.remove(part) |
|
|
| |
| if not current_restrictions: |
| updated_config = re.sub( |
| r"smtpd_sender_restrictions\s*=.*\n?", "", result, flags=re.IGNORECASE |
| ) |
|
|
| else: |
| updated_config = re.sub( |
| r"smtpd_sender_restrictions\s*=.*", |
| f"smtpd_sender_restrictions = {' '.join(current_restrictions)}", |
| result, |
| flags=re.IGNORECASE |
| ) |
|
|
| public.writeFile(self.postfix_main_cf, updated_config) |
| else: |
| ... |
| |
| else: |
| ... |
| |
|
|
| |
| shell_str = ''' |
| postmap /etc/postfix/sender_black |
| systemctl reload postfix |
| ''' |
| public.ExecShell(shell_str) |
|
|
| return True |
| except Exception as e: |
| print(f"Error managing recipient blacklist: {e}") |
| return False |
|
|
| |
| def add_domain_restrictions(self, ): |
| """ 域名加入黑名单 """ |
|
|
| if not os.path.exists(self.domain_restrictions): |
| public.writeFile(self.domain_restrictions, '') |
|
|
| try: |
| |
| domains = self.get_domain_name() |
|
|
| |
| add_set = {f"{domain} REJECT\n" for domain in domains} |
| try: |
| formatted_string = ''.join(add_set) |
| aa = public.writeFile(self.domain_restrictions, formatted_string) |
|
|
| except Exception as e: |
| return public.returnMsg(False, e) |
|
|
| |
| if not self._check_sender_domain_restrictions(): |
| self._domain_restrictions_switch(True) |
|
|
| shell_str = ''' |
| postmap /etc/postfix/sender_black |
| systemctl reload postfix |
| ''' |
| public.ExecShell(shell_str) |
|
|
| except: |
| public.print_log(public.get_error_info()) |
|
|
| return |
|
|
| def get_domain_name(self): |
| 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 data_list |
|
|
|
|
| def get_email_temp_list(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 |
|
|
| if "search" in args and args.search != "": |
| where_str = "name LIKE ? AND is_temp =?" |
| where_args = (f"%{args.search.strip()}%", 1) |
| else: |
| |
| where_str = "is_temp =?" |
| where_args = (1,) |
|
|
| with public.S("temp_email", '/www/vmail/postfixadmin.db') 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() |
|
|
| return {'data': data_list, 'total': len(count)} |
|
|
| def get_email_temp_render(self, args): |
| ''' |
| 所有邮件模版 渲染数据 |
| :param args: |
| :return: |
| ''' |
| cache_key = 'mail_sys:get_email_temp_render' |
| cache = public.cache_get(cache_key) |
| if cache: |
| return cache |
|
|
| with public.S("temp_email", '/www/vmail/postfixadmin.db') as obj: |
| temp = obj.where('is_temp', 1).field('id,type,render').select() |
| for t in temp: |
| if t['type']: |
| render_data = '' |
| if os.path.exists(t['render']): |
| render_data = public.readFile(t['render']) |
|
|
| t['render_data'] = render_data |
| public.cache_set(cache_key, data, 120) |
| return temp |
| def get_email_temp(self, args): |
| ''' |
| 邮件模版列表 无分页 |
| :param args: |
| :return: |
| ''' |
|
|
| with public.S("temp_email", '/www/vmail/postfixadmin.db') as obj: |
| data_list = obj.order('created', 'DESC').where('is_temp =?', (1,)).field('id,name').select() |
| return data_list |
|
|
|
|
|
|
| def add_email_temp(self, args): |
| ''' |
| 新增邮件模版 |
| :param args: |
| :return: |
| ''' |
|
|
| if not hasattr(args, 'temp_name'): |
| return public.returnMsg(False, public.lang('参数错误: temp_name')) |
| if not hasattr(args, 'type'): |
| return public.returnMsg(False, public.lang('参数错误: type')) |
|
|
| temp_type = int(args.type) |
| name = args.temp_name |
| timestimp = int(time.time()) |
|
|
| path = "{}/content".format(self.in_bulk_path) |
|
|
| |
| content_s = name+'content'+str(timestimp)+public.GetRandomString(5) |
| render_s = name+'render'+str(timestimp)+public.GetRandomString(5) |
|
|
| content_name = public.md5(content_s) |
| render_name = public.md5(render_s) |
| content_path = "{}/{}".format(path, content_name) |
| render_path = "{}/{}".format(path, render_name) |
|
|
| content = """<table width="500px" style="margin: 0px auto;"><tr><td><div style="background-color: transparent; padding: 0px;"><table style="width: 100%;"><tr><td style="width: 100%;"></td></tr></table></div></td></tr><tr><td><div style="background-color: transparent; padding: 0px;"><table style="width: 100%;"><tr><td style="width: 100%; text-align: center; padding: 10px;"><div style="text-align: center; padding: 10px;"><div style="border-top: 1px solid rgb(187, 187, 187); width: 100%; display: inline-block; line-height: 1px; height: 0px; vertical-align: middle;"></div></div><a href="__UNSUBSCRIBE_URL__" target="_blank" style="padding: 10px 20px; border-radius: 4px; width: auto; line-height: 120%; color: rgb(162, 162, 162); display: inline-block; background-color: rgb(255, 255, 255); font-size: 13px; text-align: center; font-weight: normal;">Unsubscribe</a></td></tr></table></div></td></tr></table>""" |
| render = """{"version":1.3,"columns_source":["32792fd2c7","8d15a39e2d"],"column_map":{"32792fd2c7":{"type":"columns","name":"列","key":"32792fd2c7","children":["707a7e2997"]},"8d15a39e2d":{"type":"columns","name":"列","key":"8d15a39e2d","children":["a6d1c0b77c"]}},"cell_map":{"707a7e2997":{"width":"100%","key":"707a7e2997","children":[]},"a6d1c0b77c":{"width":"100%","key":"a6d1c0b77c","children":["8d5aa67a11","7c5406af20"]}},"cell_style_map":{"707a7e2997":{"style":{"background":"transparent","textAlign":"center","padding":{"more":false,"all":"10","top":"","right":"","bottom":"","left":""},"border":{"more":false,"all":"0px","top":"","right":"","bottom":"","left":""}}},"a6d1c0b77c":{"style":{"background":"transparent","textAlign":"center","padding":{"more":false,"all":"10","top":"","right":"","bottom":"","left":""},"border":{"more":false,"all":"0px","top":"","right":"","bottom":"","left":""}}}},"column_row_style_map":{"32792fd2c7":{"style":{"backgroundColor":"transparent","padding":{"more":false,"all":"0px","top":"","right":"","bottom":"","left":""}}},"8d15a39e2d":{"style":{"backgroundColor":"transparent","padding":{"more":false,"all":"0px","top":"","right":"","bottom":"","left":""}}}},"comp_style_map":{"7c5406af20":{"style":{"border":{"more":false,"all":"","top":"","right":"","bottom":"","left":""},"padding":{"more":true,"all":"","top":"10px","left":"20px","right":"20px","bottom":"10px"},"borderRadius":{"more":false,"all":"4px","top":"","left":"","right":"","bottom":""},"width":"auto","lineHeight":"120%","color":"#A2A2A2FF","display":"inline-block","backgroundColor":"#FFFFFFFF","FontWeight":"normal","fontSize":"13px","textAlign":"center","LetterSpacing":"0px","fontWeight":"normal"},"general":{"textAlign":"center","padding":{"more":false,"all":"10px","top":"4px","left":"10px","right":"10px","bottom":"10px"}},"info":{"href":"__UNSUBSCRIBE_URL__","target":"_blank"},"content":"Unsubscribe"},"8d5aa67a11":{"style":{"borderTop":"1px solid #bbbbbb","width":"100%","display":"inline-block","lineHeight":"1px","height":"0px","verticalAlign":"middle"},"general":{"textAlign":"center","padding":{"more":false,"all":"10px","top":"","left":"","right":"","bottom":""}},"info":{}}},"compOptions":{},"comp_map":{"7c5406af20":{"key":"7c5406af20","type":"button"},"8d5aa67a11":{"key":"8d5aa67a11","type":"divider"}}}""" |
| public.writeFile(content_path, content) |
| public.writeFile(render_path, render) |
|
|
|
|
| |
| |
| insert_data = { |
| "name": name, |
| "content": content_path, |
| "created": timestimp, |
| "modified": timestimp, |
| "render": render_path, |
| "is_temp": 1, |
| "type": temp_type |
| } |
| try: |
| with public.S("temp_email", '/www/vmail/postfixadmin.db') as obj: |
| tmp_id = obj.insert(insert_data) |
| except: |
| public.print_log(public.get_error_info()) |
|
|
| insert_data['id'] = tmp_id |
| res = { |
| "result": public.lang("Added successfully"), |
| "data": insert_data, |
| } |
|
|
| |
| public.cache_remove('mail_sys:get_email_temp_render') |
| public.set_module_logs('mailModel', 'add_email_temp', 1) |
| return public.returnMsg(True, res) |
|
|
|
|
| def del_email_temp(self, args): |
| ''' |
| 删除邮件模版 |
| :param args: |
| :return: |
| ''' |
| tmp_id = args.id |
| with public.S("temp_email", '/www/vmail/postfixadmin.db') as obj: |
| obj.where_in('id', tmp_id.split(',')).delete() |
| |
| public.cache_remove('mail_sys:get_email_temp_render') |
| public.set_module_logs('mailModel', 'del_email_temp', 1) |
| return public.returnMsg(True,public.lang("删除成功")) |
|
|
| def edit_email_temp(self, args): |
| ''' |
| 编辑邮件模版 |
| :param args: |
| :return: |
| ''' |
|
|
| if not hasattr(args, 'id'): |
| return public.returnMsg(False, public.lang('参数错误: id')) |
| tmp_id = args.id |
| timestimp = int(time.time()) |
|
|
| if not hasattr(args, 'content'): |
| args.content = None |
|
|
| if not hasattr(args, 'upload_path'): |
| args.upload_path = None |
| if not hasattr(args, 'render'): |
| args.render = '' |
| if not hasattr(args, 'temp_name'): |
| args.temp_name = None |
|
|
| |
| name = args.temp_name |
| content = args.content |
| render = args.render |
| upload_path = args.upload_path |
| if upload_path: |
| if os.path.exists(args.upload_path): |
| content = public.readFile(args.upload_path) |
| |
| temp_type = 1 if render else 0 |
| with public.S("temp_email", '/www/vmail/postfixadmin.db') as obj: |
| tmail = obj.where('id', tmp_id).find() |
|
|
| if name: |
| update_data = { |
| "name": name, |
| "modified": timestimp, |
| "type": temp_type, |
| } |
| else: |
| update_data = { |
| "modified": timestimp, |
| "type": temp_type, |
| } |
| obj.where('id', tmp_id).update(update_data) |
|
|
| if content: |
| |
| content_path = tmail['content'] |
| render_path = tmail['render'] |
| public.writeFile(content_path, content) |
| public.writeFile(render_path, render) |
|
|
| |
| public.cache_remove('mail_sys:get_email_temp_render') |
| public.set_module_logs('mailModel', 'edit_email_temp', 1) |
| return public.returnMsg(True, public.lang("Edit success")) |
|
|
|
|
| def resolve_dns(self, domain, dns_servers=None, record_type='A'): |
| if re.match(self.REGEX_IP, domain): |
| |
| return domain |
| else: |
| resolver = dns.resolver.Resolver() |
| if dns_servers: |
| resolver.nameservers = dns_servers |
| for i in range(self.CONF_DNS_TRIES): |
| try: |
|
|
| response = resolver.resolve(domain, record_type, lifetime=self.CONF_DNS_DURATION) |
| result = str(response.rrset[0]) |
| return result |
| except (dns.resolver.NXDOMAIN, dns.resolver.YXDOMAIN, dns.resolver.Timeout, dns.resolver.NoAnswer) as e: |
| |
| if i < self.CONF_DNS_TRIES - 1: |
| time.sleep(1) |
| continue |
| else: |
| |
| return None |
| except Exception as e: |
| public.print_log(public.get_error_info()) |
| public.print_log(f"Unexpected error resolving {domain}: {e}") |
| return None |
|
|
|
|
| def check_blacklists(self, args): |
| """ |
| 检查给定的域名或IP地址是否被列入黑名单 |
| """ |
|
|
| |
| |
|
|
| |
| |
| |
|
|
| |
| if not hasattr(args, 'a_record'): |
| return public.returnMsg(False, public.lang('参数错误: a_record')) |
|
|
| |
| domain = args.a_record |
|
|
| dns_servers = ['8.8.8.8', '1.1.1.1'] |
|
|
| blacklist_file = None |
|
|
| |
| if blacklist_file: |
| with open(blacklist_file, 'r') as f: |
| self.CONF_BLACKLISTS.extend(f.read().splitlines()) |
|
|
| ip = self.resolve_dns(domain, dns_servers) |
| if not ip: |
| |
|
|
| return public.returnMsg(False, public.lang('Error: 没有找到对应的DNS记录 {} ', domain)) |
| if ip == '127.0.0.1': |
| ip = public.GetLocalIp() |
|
|
| self.run_thread(self._check_blacklists, (ip, domain,dns_servers)) |
| public.set_module_logs('mailModel', 'check_blacklists', 1) |
|
|
| return public.returnMsg(True, public.lang('检查需要两分钟,请耐心等待')) |
|
|
|
|
| def _check_blacklists(self, ip, domain, dns_servers): |
| ''' |
| 域名黑名单检测 + 告警 |
| :param args: ip str 域名 |
| :param args: blcheck_info dict 黑名单检测统计 |
| :return: |
| ''' |
|
|
| |
| reversed_ip = '.'.join(reversed(ip.split('.'))) |
|
|
| blacklisted = 0 |
| invalid = 0 |
| passed = 0 |
|
|
| black_list = [] |
|
|
| |
| domain_check_log = f'/www/server/panel/plugin/mail_sys/data/{domain}_blcheck.txt' |
| |
| public.writeFile(domain_check_log, '') |
|
|
| |
| date = datetime.now().strftime("%Y-%m-%d %H:%M:%S") |
| check_log = f'{date}: Start checking... ' |
| public.AppendFile(domain_check_log, check_log + '\n') |
|
|
| for blacklist in self.CONF_BLACKLISTS: |
| times = int(time.time()) |
| date = datetime.now().strftime("%Y-%m-%d %H:%M:%S") |
| test_domain = f"{reversed_ip}.{blacklist}" |
| |
| result = self.resolve_dns(test_domain, dns_servers) |
| |
| if not result: |
| check_log = f'{date}: {blacklist} ----------------------------- √' |
| public.AppendFile(domain_check_log, check_log + '\n') |
|
|
| passed += 1 |
| elif result.startswith('127.'): |
| if result == '127.255.255.254': |
| passed += 1 |
| check_log = f'{date}: {blacklist} ----------------------------- √ ({result})' |
| public.AppendFile(domain_check_log, check_log + '\n') |
| else: |
| |
| check_log = f'{date}: {blacklist} ----------------------------- x blacklisted ({result})' |
| public.AppendFile(domain_check_log, check_log + '\n') |
| blacklisted += 1 |
| black_list.append({"blacklist": blacklist, "time": times}) |
| else: |
| |
| check_log = f'{date}: {blacklist} ----------------------------- Invalid' |
| public.AppendFile(domain_check_log, check_log + '\n') |
| invalid += 1 |
|
|
| date = datetime.now().strftime("%Y-%m-%d %H:%M:%S") |
| check_log = f'--------------------------------------------------------------------------------------- \n' \ |
| f'Results for {domain}: \n' \ |
| f'Ip: {ip} \n' \ |
| f'Tested: {len(self.CONF_BLACKLISTS)} \n' \ |
| f'Passed: {passed} \n' \ |
| f'Invalid: {invalid} \n' \ |
| f'Blacklisted: {blacklisted} \n' \ |
| f'--------------------------------------------------------------------------------------- \n' \ |
| f'{date}: Check finished' |
|
|
| public.AppendFile(domain_check_log, check_log) |
|
|
|
|
| data = { |
| "time": int(time.time()), |
| "results": domain, |
| "ip": ip, |
| "tested": len(self.CONF_BLACKLISTS), |
| "passed": passed, |
| "invalid": invalid, |
| "blacklisted": blacklisted, |
| "black_list": black_list |
| } |
|
|
| |
| self.add_blacklist(domain, data) |
|
|
| |
| if blacklisted > 0: |
| args = public.dict_obj() |
| args.keyword = 'mail_domain_black' |
| send_task = self.get_alarm_send(args) |
| if send_task and send_task.get('status', False): |
| black_lists = [i['blacklist'] for i in black_list] |
|
|
| body = [f">Send content: Your IP [{ip}] is on the email blacklist.", f">Results for {domain}.", |
| f">Blacklisted: {black_lists}."] |
|
|
| |
| args.body = body |
| args.domain = domain |
| self.send_mail_data(args) |
|
|
| return data |
|
|
|
|
| def add_blacklist(self, domain, blcheck_info): |
| ''' |
| 记录域名黑名单检测内容 |
| :param args: domain str 域名 |
| :param args: blcheck_info dict 黑名单检测统计 |
| :return: |
| ''' |
|
|
| path = self.blcheck_count |
| data = {} |
| if os.path.exists(path): |
| data = public.readFile(path) |
| try: |
| data = json.loads(data) |
| except: |
| pass |
| data[domain] = blcheck_info |
| public.writeFile(path, public.GetJson(data)) |
|
|
| return True |
|
|
| |
| def check_domain_blacklist_corn(self): |
| ''' |
| 执行检查域名黑明定的定时任务 |
| :param |
| :return: |
| ''' |
| |
| |
|
|
| |
| blacklist_alarm_switch = '/www/server/panel/plugin/mail_sys/data/blacklist_alarm_switch' |
| if os.path.exists(blacklist_alarm_switch): |
| return |
|
|
| endtime = public.get_pd()[1] |
| curtime = int(time.time()) |
|
|
| |
| if endtime != 0 and endtime < curtime: |
| print("|-This feature is exclusive to the Pro version") |
| return |
| domain_list = self.M('domain').order('created desc').field('a_record,domain').select() |
| if not domain_list: |
| print("|-The domain is empty, skip") |
| return |
|
|
| |
| |
| |
| |
| |
|
|
| interval_time = 3*3600 |
| if len(domain_list) > 8: |
| interval_time = int((24 / len(domain_list)) * 3600) |
|
|
| for i in domain_list: |
| |
| domain = i['a_record'] |
| dns_servers = None |
|
|
| ip = self.resolve_dns(domain, dns_servers) |
| if not ip: |
| |
| print(f"|-Error: No DNS record found for {domain}") |
| continue |
|
|
| data = self._check_blacklists(ip, domain, dns_servers) |
| |
| time.sleep(interval_time) |
|
|
| return |
|
|
| def send_mail_data(self, args): |
|
|
| body = args.body |
|
|
| keyword = args.keyword |
| push_data = {} |
| if keyword == 'mail_domain_black': |
| domain = args.domain |
| push_data = { |
| "domain": domain, |
| "msg_list": body |
| } |
| if keyword == 'mail_server_status': |
| push_data = { |
| "msg_list": body |
| } |
|
|
| try: |
| import sys |
| if "/www/server/panel" not in sys.path: |
| sys.path.insert(0, "/www/server/panel") |
|
|
| from mod.base.push_mod import push_by_task_keyword |
| res = push_by_task_keyword(keyword, keyword, push_data=push_data) |
| if res: |
| return |
| except: |
| pass |
|
|
|
|
|
|
| |
| def get_alarm_send(self, args): |
| |
| keyword = args.keyword |
| task_file_path = '/www/server/panel/data/mod_push_data/task.json' |
| sender_file_path = '/www/server/panel/data/mod_push_data/sender.json' |
| task_data = {} |
|
|
| try: |
| with open(task_file_path, 'r') as file: |
| tasks = json.load(file) |
|
|
| |
| with open(sender_file_path, 'r') as file: |
| senders = json.load(file) |
| sender_dict = {sender['id']: sender for sender in senders} |
|
|
| |
| for task in tasks: |
| if task.get('keyword') == keyword: |
| task_data = task |
| sender_types = set() |
|
|
| |
| for sender_id in task.get('sender', []): |
| if sender_id in sender_dict: |
| sender_types.add(sender_dict[sender_id]['sender_type']) |
|
|
| |
| task_data['channels'] = list(sender_types) |
| break |
|
|
| except Exception as e: |
| return False |
| if task_data: |
| return task_data |
| else: |
| return False |
|
|
|
|
| |
| def export_email_template(self, args): |
| if not hasattr(args, 'ids'): |
| return public.returnMsg(False, public.lang('参数错误: ids')) |
| ids_list = args.ids.split(',') |
| ids_list = [int(id_str) for id_str in ids_list] |
| with public.S("temp_email", '/www/vmail/postfixadmin.db') as obj: |
| temps = obj.where_in('id', ids_list).select() |
|
|
| |
| |
| current_time = datetime.now() |
| timestamp = current_time.strftime("%Y%m%d%H%M%S") |
| template_p = f"t_{timestamp}" |
| |
| download_dir = f"/tmp/export_{template_p}" |
| os.makedirs(download_dir) |
|
|
| for i in temps: |
| rdm = public.GetRandomString(5) |
| download_tmpdir = f'{download_dir}/{template_p}_{rdm}' |
|
|
| info = {'type': i['type'], 'name': i['name']} |
| content_path = i['content'] |
| render_path = i['render'] |
|
|
| content_new = os.path.join(download_tmpdir, 'content') |
| render_new = os.path.join(download_tmpdir, 'render') |
| template_info = os.path.join(download_tmpdir, 'template_info.json') |
| if not os.path.exists(os.path.dirname(content_new)): |
| os.makedirs(os.path.dirname(content_new)) |
| if not os.path.exists(os.path.dirname(render_new)): |
| os.makedirs(os.path.dirname(render_new)) |
|
|
| if os.path.isfile(content_path): |
| shutil.copy2(content_path, content_new) |
| if os.path.isfile(render_path): |
| shutil.copy2(render_path, render_new) |
|
|
| public.writeFile(template_info, json.dumps(info)) |
| |
| zip_path = f"{download_dir}.zip" |
| |
| with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: |
| |
| for root, dirs, files in os.walk(download_dir): |
| for file in files: |
| full_file_path = os.path.join(root, file) |
| arcname = os.path.relpath(full_file_path, download_dir) |
| zipf.write(full_file_path, arcname) |
|
|
| |
| shutil.rmtree(download_dir) |
|
|
| return public.returnMsg(True, zip_path) |
|
|
| |
| def import_email_template(self, args): |
| upload_dir = args.path |
| |
|
|
| |
| if not os.path.exists(upload_dir): |
| return public.returnMsg(False, public.lang('The file {} does not exist',upload_dir)) |
|
|
| |
| upload_path = '/tmp/upload_mail_template' |
| if os.path.exists(upload_path): |
| shutil.rmtree(upload_path) |
|
|
| public.ExecShell('unzip -o "' + upload_dir + '" -d ' + upload_path + '/') |
|
|
| insert_data = [] |
| for p_name in os.listdir(upload_path): |
| tmppath = os.path.join(upload_path, p_name) |
|
|
| cur_tmpinfo = {} |
|
|
| |
| for p_name in os.listdir(tmppath): |
| cur_file = os.path.join(tmppath, p_name) |
|
|
| if not os.path.exists(cur_file): |
| continue |
|
|
| if p_name == 'content': |
| cur_tmpinfo['content_data'] = public.readFile(cur_file) |
| if p_name == 'render': |
| cur_tmpinfo['render_data'] = public.readFile(cur_file) |
|
|
| if p_name == 'template_info.json': |
| info = json.loads(public.readFile(cur_file)) |
| cur_tmpinfo['name'] = info['name'] |
| cur_tmpinfo['type'] = int(info.get('type', 0)) |
|
|
| path = "{}/content".format(self.in_bulk_path) |
|
|
| |
| timestimp = int(time.time()) |
| content_s = cur_tmpinfo['name'] + 'content' + str(timestimp) + public.GetRandomString(5) |
| render_s = cur_tmpinfo['name'] + 'render' + str(timestimp) + public.GetRandomString(5) |
|
|
| content_name = public.md5(content_s) |
| render_name = public.md5(render_s) |
| cur_tmpinfo['content_path'] = "{}/{}".format(path, content_name) |
| cur_tmpinfo['render_path'] = "{}/{}".format(path, render_name) |
| cur_tmpinfo['timestimp'] = timestimp |
|
|
| data = { |
| "name": cur_tmpinfo['name'], |
| "content": cur_tmpinfo['content_path'], |
| "created": cur_tmpinfo['timestimp'], |
| "modified": cur_tmpinfo['timestimp'], |
| "render": cur_tmpinfo['render_path'], |
| "is_temp": 1, |
| "type": cur_tmpinfo['type'] |
| } |
| insert_data.append(data) |
|
|
| public.writeFile(cur_tmpinfo['content_path'], cur_tmpinfo['content_data']) |
| public.writeFile(cur_tmpinfo['render_path'], cur_tmpinfo['render_data']) |
| try: |
| with public.S("temp_email", '/www/vmail/postfixadmin.db') as obj: |
| add_num = obj.insert_all(insert_data, option='IGNORE') |
| except: |
| public.print_log(public.get_error_info()) |
|
|
| return public.returnMsg(True, public.lang('Import successfully!')) |
|
|
| |
| def retrieve_email_address_from_groups(self, args: public.dict_obj): |
| pass |
|
|
|
|
| |
| def check_new_unsubscribe(self, ): |
| """ 检查退订页面是否拉取""" |
| |
| if not os.path.exists('/www/vmail'): |
| return |
| |
| |
| |
| |
|
|
| unsubscribe_path = '/www/server/panel/BTPanel/templates/default/unsubscribe.html' |
| if os.path.exists(unsubscribe_path): |
| return |
| else: |
| |
| public.downloadFile('https://node.aapanel.com/mail_sys/unsubscribe.html', unsubscribe_path) |
|
|
| |
| |
|
|
|
|