diff --git a/README.md b/README.md index e98a79f..05fab35 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ - [ ] QQ - [ ] 钉钉 - [ ] 飞书 + - [x] Gmail # 快速开始 @@ -217,3 +218,17 @@ Hit Ctrl-C to quit. } } ``` + +### 5. Gmail +**需要:** 一个服务器、一个Gmail account +Follow [官方文档](https://support.google.com/mail/answer/185833?hl=en) to create APP password for google account, config as below, then cheers!!! +```json +"channel": { + "type": "gmail", + "gmail": { + "subject_keyword": ["bot", "@bot"], + "host_email": "xxxx@gmail.com", + "host_password": "GMAIL ACCESS KEY" + } + } +``` \ No newline at end of file diff --git a/channel/channel_factory.py b/channel/channel_factory.py index dd62228..693aae1 100644 --- a/channel/channel_factory.py +++ b/channel/channel_factory.py @@ -25,5 +25,9 @@ def create_channel(channel_type): from channel.wechat.wechat_mp_service_channel import WechatServiceAccount return WechatServiceAccount() + elif channel_type == const.GMAIL: + from channel.gmail.gmail_channel import GmailChannel + return GmailChannel() + else: raise RuntimeError diff --git a/channel/gmail/gmail_channel.py b/channel/gmail/gmail_channel.py new file mode 100755 index 0000000..4df03e8 --- /dev/null +++ b/channel/gmail/gmail_channel.py @@ -0,0 +1,180 @@ +import smtplib +import imaplib +import email +import re +import base64 +import time +from random import randrange +from email.mime.text import MIMEText +from email.header import decode_header +from channel.channel import Channel +from concurrent.futures import ThreadPoolExecutor +from common import const +from config import channel_conf_val, channel_conf + + +smtp_ssl_host = 'smtp.gmail.com: 587' +imap_ssl_host = 'imap.gmail.com' +MAX_DELAY = 30 +MIN_DELAY = 15 +STEP_TIME = 2 +LATESTN = 5 +wait_time = 0 +thread_pool = ThreadPoolExecutor(max_workers=8) + +def checkEmail(email): + # regex = '^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,3}$' + regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' + if re.search(regex, email): + return True + else: + return False + +def process(max, speed): + global wait_time + i=0 + while i<=max: + i=i+1 + time.sleep(speed) + print("\r"+"Waited: "+str(i+wait_time)+"s", end='') + # print("\r"+"==="*int(i-1)+":-)"+"==="*int(max-i)+"$"+str(max)+' waited:'+str(i)+"%", end='') + wait_time += max*speed + +class GmailChannel(Channel): + def __init__(self): + self.host_email = channel_conf_val(const.GMAIL, 'host_email') + self.host_password = channel_conf_val(const.GMAIL, 'host_password') + # self.addrs_white_list = channel_conf_val(const.GMAIL, 'addrs_white_list') + self.subject_keyword = channel_conf_val(const.GMAIL, 'subject_keyword') + + def startup(self): + global wait_time + ques_list = list() + lastques = {'from': None, 'subject': None, 'content': None} + print("INFO: let's go...") + while(True): + ques_list = self.receiveEmail() + if ques_list: + for ques in ques_list: + if ques['subject'] is None: + print("WARN: question from:%s is empty " % ques['from']) + elif(lastques['subject'] == ques['subject'] and lastques['from'] == ques['from']): + print("INFO: this question has already been answered. Q:%s" % (ques['subject'])) + else: + if ques['subject']: + print("Nice: a new message coming...", end='\n') + self.handle(ques) + lastques = ques + wait_time = 0 + else: + print("WARN: the question in subject is empty") + else: + process(randrange(MIN_DELAY, MAX_DELAY), STEP_TIME) + + def handle(self, question): + message = dict() + context = dict() + print("INFO: From: %s Question: %s" % (question['from'], question['subject'])) + context['from_user_id'] = question['from'] + answer = super().build_reply_content(question['subject'], context) #get answer from openai + message = MIMEText(answer) + message['subject'] = question['subject'] + message['from'] = self.host_email + message['to'] = question['from'] + thread_pool.submit(self.sendEmail, message) + + def sendEmail(self, message: list) -> dict: + smtp_server = smtplib.SMTP(smtp_ssl_host) + smtp_server.starttls() + smtp_server.login(self.host_email, self.host_password) + output = {'success': 0, 'failed': 0, 'invalid': 0} + try: + smtp_server.sendmail(message['from'], message['to'], message.as_string()) + print("sending to {}".format(message['to'])) + output['success'] += 1 + except Exception as e: + print("Error: {}".format(e)) + output['failed'] += 1 + print("successed:{}, failed:{}".format(output['success'], output['failed'])) + smtp_server.quit() + return output + + def receiveEmail(self): + question_list = list() + question = {'from': None, 'subject': None, 'content': None} + imap_server = imaplib.IMAP4_SSL(imap_ssl_host) + imap_server.login(self.host_email, self.host_password) + imap_server.select('inbox') + status, data = imap_server.search(None, 'ALL') + mail_ids = [] + for block in data: + mail_ids += block.split() + #only fetch the latest 5 messages + mail_ids = mail_ids[-LATESTN:] + for i in mail_ids: + status, data = imap_server.fetch(i, '(RFC822)') + for response in data: + if isinstance(response, tuple): + message = email.message_from_bytes(response[1]) + mail_from = message['from'].split('<')[1].replace(">", "") + # if mail_from not in self.addrs_white_list: + # continue + + #subject do not support chinese + mail_subject = decode_header(message['subject'])[0][0] + if isinstance(mail_subject, bytes): + # UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc5 + try: + mail_subject = mail_subject.decode() + except UnicodeDecodeError: + mail_subject = mail_subject.decode('latin-1') + if not self.check_contain(mail_subject, self.subject_keyword): #check subject here + continue + if message.is_multipart(): + mail_content = '' + for part in message.get_payload(): + flag=False + if isinstance(part.get_payload(), list): + part = part.get_payload()[0] + flag = True + if part.get_content_type() in ['text/plain', 'multipart/alternative']: + #TODO some string can't be decode + if flag: + mail_content += str(part.get_payload()) + else: + try: + mail_content += base64.b64decode(str(part.get_payload())).decode("utf-8") + except UnicodeDecodeError: + mail_content += base64.b64decode(str(part.get_payload())).decode('latin-1') + else: + mail_content = message.get_payload() + question['from'] = mail_from + question['subject'] = ' '.join(mail_subject.split(' ')[1:]) + question['content'] = mail_content + # print(f'\nFrom: {mail_from}') + print(f'\n\nSubject: {mail_subject}') + # print(f'Content: {mail_content.replace(" ", "")}') + question_list.append(question) + question = {'from': None, 'subject': None, 'content': None} + imap_server.store(i, "+FLAGS", "\\Deleted") #delete the mail i + print("INFO: deleting mail: %s" % mail_subject) + imap_server.expunge() + imap_server.close() + imap_server.logout() + return question_list + + def check_contain(self, content, keyword_list): + if not keyword_list: + return None + for ky in keyword_list: + if content.find(ky) != -1: + return True + return None + + + + + + + + diff --git a/common/const.py b/common/const.py index 6b10525..17ecc4d 100644 --- a/common/const.py +++ b/common/const.py @@ -3,6 +3,7 @@ TERMINAL = "terminal" WECHAT = "wechat" WECHAT_MP = "wechat_mp" WECHAT_MP_SERVICE = "wechat_mp_service" +GMAIL = "gmail" # model OPEN_AI = "openai" diff --git a/config-template.json b/config-template.json index c30b210..c2127f0 100644 --- a/config-template.json +++ b/config-template.json @@ -21,6 +21,12 @@ "wechat_mp": { "token": "YOUR TOKEN", "port": "8088" + }, + + "gmail": { + "subject_keyword": ["bot", "@bot"], + "host_email": "xxxx@gmail.com", + "host_password": "GMAIL ACCESS KEY" } } }