├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── README.md ├── README_sc.md ├── README_tc.md ├── images ├── gmail_filter_keys.png ├── gmail_filter_setting.png ├── gmail_forward_setting.png ├── mailparser_data_parsing_rules.png ├── mailparser_data_parsing_rules_pin.png ├── mailparser_data_parsing_rules_receiver.png ├── mailparser_data_parsing_rules_sender.png ├── mailparser_data_parsing_rules_subject.png ├── mailparser_inbox_setting_1.png ├── mailparser_inbox_setting_2.png ├── mailparser_parsed_data_downloads.png ├── mailparser_parsed_data_downloads_setting.png └── the_final_effect.png ├── main.py └── serverless_function ├── README.md └── main.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | English | [简体中文](README_sc.md) | [正體中文](README_tc.md) 2 | # eu_ex 3 | 4 | eu_ex means EUserv_extend. A Python script which can help you renew your free EUserv IPv6 VPS. 5 | 6 | This Script can check the VPS amount in your account automatically and renew the VPS if it can be renewed. 7 | 8 | ## How to Use 9 | 10 | 1. Install Python3 and dependences, the following command is used in debian/ubuntu for example, 11 | 12 | ```bash 13 | #Install Python3 14 | apt install python3 python3-pip -y 15 | #Intstall dependences 16 | pip install requests beautifulsoup4 17 | ``` 18 | 19 | 2. It is not recommended to replace the `USERNAME` & `PASSWORD` parameters with yours in `main.py` Line 37-38 directly. Pass them in from environment variables. 20 | 21 | Your can add multiple accounts with single space separated. 22 | 23 | 3. Your can add multiple mailparser.io parsed data download URL id with single space separated. The download URL id is in `https://files.mailparser.io/d/`. 24 | 25 | 4. Pass the **Actions secrets** into the environment variable of your GitHub Action runtime environment. For example, the following environment variables are required. 26 | 27 | ``` 28 | env: 29 | USERNAME: ${{ secrets.USERNAME }} 30 | PASSWORD: ${{ secrets.PASSWORD }} 31 | # https://mailparser.io 32 | MAILPARSER_DOWNLOAD_URL_ID: ${{ secrets.MAILPARSER_DOWNLOAD_URL_ID }} 33 | ``` 34 | 35 | ## Mail forwarding and mailparser settings 36 | ### Mail forwarding 37 | 38 | Take gmail as an example, forward emails to [mailparser](https://mailparser.io). It is possible for non-gmail mailboxes to receive emails from euserv, provided that they can be received. Currently outlook/hotmail does not receive it. 39 | 40 | - ![gmail_filter_keys](./images/gmail_filter_keys.png) 41 | 42 | - ![gmail_filter_setting](./images/gmail_filter_setting.png) 43 | 44 | - ![gmail_forward_setting](./images/gmail_forward_setting.png) 45 | 46 | ### Mailparser settings 47 | 48 | - Create new inbox firstly. 49 | - Create data parsing rules. 50 | - mailparser_data_parsing_rules 51 | ![mailparser_data_parsing_rules](./images/mailparser_data_parsing_rules.png) 52 | - mailparser_data_parsing_rules_pin 53 | ![mailparser_data_parsing_rules_pin](./images/mailparser_data_parsing_rules_pin.png) 54 | - mailparser_data_parsing_rules_subject 55 | ![mailparser_data_parsing_rules_subject](./images/mailparser_data_parsing_rules_subject.png) 56 | - mailparser_data_parsing_rules_sender 57 | ![mailparser_data_parsing_rules_sender](./images/mailparser_data_parsing_rules_sender.png) 58 | - mailparser_data_parsing_rules_receiver 59 | ![mailparser_data_parsing_rules_receiver](./images/mailparser_data_parsing_rules_receiver.png) 60 | - Create parsed data download url 61 | - mailparser_parsed_data_downloads 62 | ![mailparser_parsed_data_downloads](./images/mailparser_parsed_data_downloads.png) 63 | - mailparser_parsed_data_downloads_setting 64 | ![mailparser_parsed_data_downloads_setting](./images/mailparser_parsed_data_downloads_setting.png) 65 | - Settings 66 | - mailparser_inbox_setting_1 67 | ![mailparser_inbox_setting_1](./images/mailparser_inbox_setting_1.png) 68 | - mailparser_inbox_setting_2 69 | ![mailparser_inbox_setting_2](./images/mailparser_inbox_setting_2.png) 70 | 71 | ## Final result 72 | The effect is as shown, 73 | 74 | ![mailparser_inbox_setting_2](./images/the_final_effect.png) 75 | 76 | ## TODO 77 | 78 | - [x] ~~Validate the `receiver` field parsed by mailparser to reduce malicious email interference.~~ Won't do due to mailparser *Inbox Settings - Email Reception*. 79 | - [x] Log internationalization and localization. 80 | - [ ] Open pre-trained models to solve the problem of CAPTCHA recognition locally when the CAPTCHA solver API is not working. 81 | - [ ] Consider euserv access timeout retry, lower priority 82 | 83 | ## Acknowledgement 84 | 85 | - Thanks EUserv provides us free IPv6 VPS for learning. 86 | - Thanks CokeMine & its repository contributors provides us the original *EUserv_extend* script .The internet never forgets, but people do. 87 | 88 | ## FAQs 89 | 90 | 1. **Q**: It can be non-gmail mailbox? 91 | 92 | **A**: Can be a non-gmail mailbox,the prerequisite is to receive emails from euserv. Currently outlook/hotmail does not receive it. 93 | 94 | 2. **Q**: Can n mailboxes use the same mailparser or do I need to apply for n mailparsers to correspond with one? 95 | 96 | **A**: The mailparser free account can set up to 10 inboxes, and these 10 inboxes can correspond to 10 euserv accounts, and there are 10 mailparser parsed data download URLs(ids). So, it depends on whether you have n>10, or n<10. n<10, one mailparser account is enough, and then the parsed data download URL ids correspond to the registered email accounts of euserv. 97 | 98 | 3. **Q**: How eu_ex script works? 99 | 100 | **A**: EUserv set the first threshold from the end of September 2021, that is, the login verification code (successful verification status maintained for 24 hours), so from now on, we use the API provided by TrueCaptcha (there is a free amount every day) to identify. Not long after, about the beginning of November 2021, EUserv set a second threshold, which is the email PIN verification when renewing, and here the solution is about two kinds: a. Login to the mailbox to get the email containing EUserv PIN. b. Convert the email into HTTP REST API to get it automatically. Option b is adopted here. It seems that only [Mailparser](https://mailparser.io) and [Zapier Emails Parser](https://parser.zapier.com/) are available in option b for free quota. Option b is clearly better than option a. 101 | 102 | ## References 103 | 104 | ### EUserv "PIN for the Confirmation of a Security Check" original mail 105 | 106 | ``` 107 | From: EUserv Support 108 | To: xyz@example.com 109 | Subject: EUserv - PIN for the Confirmation of a Security Check 110 | Content-Type: text/plain; charset = utf-8 111 | Dear XYZ, 112 | 113 | you have just requested a PIN for confirmation of a security check at EUserv. If you have not requested the PIN then ignore this email. 114 | 115 | PIN: 116 | 123456 117 | 118 | PLEASE NOTE: If you already have requested a new PIN for the same process this PIN is invalid. Also this PIN is only valid within the session in which it has been requested. This means the PIN is invalid if you for example change the browser or if you logout and perform a new login. 119 | 120 | 121 | Sincerely, 122 | Your customer support EUserv 123 | 124 | -- 125 | Web ................: http://www.euserv.com 126 | Login control panel.: https://support.euserv.com 127 | FAQ ................: http://faq.euserv.com 128 | Help & Guides.......: http://wiki.euserv.com 129 | Community / Forum...: http://forum.euserv.com 130 | Mailing-Liste ......: http://www.euserv.com/en/?show_contact=mailinglist 131 | Twitter ............: http://twitter.com/euservhosting 132 | Facebook ...........: http://www.facebook.com/euservhosting 133 | -- 134 | 135 | EUserv Internet 136 | is a division of 137 | ISPpro Internet KG 138 | 139 | Postal address: 140 | ISPpro Internet KG 141 | Division EUserv Internet 142 | P.O. Box 2224 143 | 07622 Hermsdorf 144 | GERMANY 145 | 146 | Support-Phone: +49 (0) 3641 3101011 (English speaking) 147 | 148 | Administration: 149 | ISPpro Internet KG 150 | Neue Str. 4 151 | D-07639 Bad Klosterlausnitz 152 | GERMANY 153 | 154 | Management...............: Dirk Seidel 155 | Register.................: AG Jena, HRA 202638 156 | VAT Number...............: 162/156/36600 157 | Tax office ..............: Jena 158 | International VAT Number.: DE813856317 159 | ``` 160 | 161 | -------------------------------------------------------------------------------- /README_sc.md: -------------------------------------------------------------------------------- 1 | [English](README.md) | 简体中文 | [正體中文](README_tc.md) 2 | # eu_ex 3 | 4 | eu_ex 是 EUserv_extend 的简写。一个 Python 脚本,可以帮你续期免费 EUserv IPv6 VPS。 5 | 6 | 这个脚本可以自动检查你账户中的 VPS 数量,如果可以续期,就为 VPS 续期。 7 | 8 | ## 如何使用 9 | 10 | 1. 安装 Python3 和必要的依赖,以下命令在 debian/ubuntu 为例, 11 | 12 | ```bash 13 | #Install Python3 14 | apt install python3 python3-pip -y 15 | #Intstall dependences 16 | pip install requests beautifulsoup4 17 | ``` 18 | 19 | 2. 不建议在 `main.py` 文件第 37-38 行直接用你的真实值去替换`USERNAME`和`PASSWORD`。可以使用环境变量传入。 20 | 21 | 你可以添加多个账户,并以一个空格分隔。 22 | 23 | 3. 你可以添加多个 mailparser.io 解析数据的下载 URL ID,并以一个空格分隔。下载的 URL ID 在`https://files.mailparser.io/d/`。 24 | 25 | 4. 将 **Actions secrets** 传入你的 GitHub Action 运行环境的环境变量。例如,以下环境变量是必需的。 26 | 27 | ``` 28 | env: 29 | USERNAME: ${{ secrets.USERNAME }} 30 | PASSWORD: ${{ secrets.PASSWORD }} 31 | # https://mailparser.io 32 | MAILPARSER_DOWNLOAD_URL_ID: ${{ secrets.MAILPARSER_DOWNLOAD_URL_ID }} 33 | ``` 34 | 35 | ## 邮件转发和 mailparser 设置 36 | ### 邮件转发 37 | 38 | 以 gmail 为例, 转发邮件至 [mailparser](https://mailparser.io)。可以是非 gmail 邮箱,前提是可以收到 euserv 的邮件。目前 outlook/hotmail 是收不到的。 39 | 40 | - ![gmail_filter_keys](./images/gmail_filter_keys.png) 41 | 42 | - ![gmail_filter_setting](./images/gmail_filter_setting.png) 43 | 44 | - ![gmail_forward_setting](./images/gmail_forward_setting.png) 45 | 46 | ### Mailparser 设置 47 | 48 | - 首先创建新的收件箱。 49 | - 创建数据解析规则。 50 | - 数据解析规则,pin 为必需,其他可选。 51 | ![mailparser_data_parsing_rules](./images/mailparser_data_parsing_rules.png) 52 | - pin 的解析规则 53 | ![mailparser_data_parsing_rules_pin](./images/mailparser_data_parsing_rules_pin.png) 54 | - subject 的解析规则 55 | ![mailparser_data_parsing_rules_subject](./images/mailparser_data_parsing_rules_subject.png) 56 | - sender 的解析规则 57 | ![mailparser_data_parsing_rules_sender](./images/mailparser_data_parsing_rules_sender.png) 58 | - receiver 的解析规则 59 | ![mailparser_data_parsing_rules_receiver](./images/mailparser_data_parsing_rules_receiver.png) 60 | - 创建解析数据下载 URL 61 | - 解析数据下载 URL 62 | ![mailparser_parsed_data_downloads](./images/mailparser_parsed_data_downloads.png) 63 | - 解析数据下载设置 64 | ![mailparser_parsed_data_downloads_setting](./images/mailparser_parsed_data_downloads_setting.png) 65 | - mailparser 收件箱设置(可选,为减少收到 spam 邮件的风险,最好设置) 66 | - mailparser 收件箱设置 1 67 | ![mailparser_inbox_setting_1](./images/mailparser_inbox_setting_1.png) 68 | - mailparser 收件箱设置 2 69 | ![mailparser_inbox_setting_2](./images/mailparser_inbox_setting_2.png) 70 | 71 | ## 最终效果 72 | 效果如图, 73 | 74 | ![mailparser_inbox_setting_2](./images/the_final_effect.png) 75 | 76 | ## 待办事项 77 | 78 | - [x] ~~验证 mailparser 解析的`receiver'字段,以减少恶意邮件的干扰。~~ 由于 mailparser *Inbox Settings - Email Reception*,所以不做了。 79 | - [x] 日志国际化和本地化。 80 | - [ ] 当验证码识别 API 失效时开放预训练的模型,在本地,不调用第三方接口就解决验证码识别的问题。 81 | - [ ] 考虑 euserv 访问超时重试,较低优先级。 82 | 83 | ## 鸣谢 84 | 85 | - 感谢 EUserv 提供免费的 IPv6 VPS 供我们学习。 86 | - 感谢 CokeMine 和其仓库贡献者为我们提供的最初 *EUserv_extend* 脚本。互联网永远不会忘记,但人们会。 87 | 88 | ## FAQs 89 | 90 | 1. **Q**: 可以非 gmail 邮箱吗? 91 | 92 | **A**: 可以是非 gmail 邮箱,前提是可以收到 euserv 的邮件。目前 outlook/hotmail 是收不到的。 93 | 94 | 2. **Q**: n 个邮箱能在用同一个 mailparser 还是需要申请 n 个 mailparser 与之一 一对应吗? 95 | 96 | **A**: mailparser free 账号最多可以设置 10 个收件箱,这 10 个收件箱就可以对应 10 个 euserv 账号,也就有了 10 个 mailparser parsed data download URL(id)。所以,这得看你 n>10, 还是 n<10 了。n<10, 一个 mailparser 账号即可,然后 parsed data download URL id 一 一对应 euserv 的注册邮箱账号。 97 | 98 | 3. **Q**: 续期脚本是如何工作的? 99 | 100 | **A**: EUserv 从 2021 年 9 月底开始,设置了第一道门槛,那就是登录出现验证码(成功验证状态维持 24 小时),所以目前就用 TrueCaptcha 提供的 API(每天都有免费额度) 进行识别。没过多久,大概是 2021 年11月初,EUserv 又设置了第二道门槛,就是续期时的邮件 PIN 码验证,这里解决办法就大概两种:a. 登录邮箱获取含 EUserv PIN 的邮件。b. 将邮件转化为 HTTP REST API,自动获取。这里采取的是方案 b。方案 b 中可选的存在免费额度的,好像目前仅有 [Mailparser](https://mailparser.io) 和 [Zapier Emails Parser](https://parser.zapier.com/)。方案 b 明显要优于方案 a。 101 | 102 | ## 参考信息 103 | 104 | ### EUserv "PIN for the Confirmation of a Security Check" 原始邮件 105 | 106 | ``` 107 | From: EUserv Support 108 | To: xyz@example.com 109 | Subject: EUserv - PIN for the Confirmation of a Security Check 110 | Content-Type: text/plain; charset = utf-8 111 | Dear XYZ, 112 | 113 | you have just requested a PIN for confirmation of a security check at EUserv. If you have not requested the PIN then ignore this email. 114 | 115 | PIN: 116 | 123456 117 | 118 | PLEASE NOTE: If you already have requested a new PIN for the same process this PIN is invalid. Also this PIN is only valid within the session in which it has been requested. This means the PIN is invalid if you for example change the browser or if you logout and perform a new login. 119 | 120 | 121 | Sincerely, 122 | Your customer support EUserv 123 | 124 | -- 125 | Web ................: http://www.euserv.com 126 | Login control panel.: https://support.euserv.com 127 | FAQ ................: http://faq.euserv.com 128 | Help & Guides.......: http://wiki.euserv.com 129 | Community / Forum...: http://forum.euserv.com 130 | Mailing-Liste ......: http://www.euserv.com/en/?show_contact=mailinglist 131 | Twitter ............: http://twitter.com/euservhosting 132 | Facebook ...........: http://www.facebook.com/euservhosting 133 | -- 134 | 135 | EUserv Internet 136 | is a division of 137 | ISPpro Internet KG 138 | 139 | Postal address: 140 | ISPpro Internet KG 141 | Division EUserv Internet 142 | P.O. Box 2224 143 | 07622 Hermsdorf 144 | GERMANY 145 | 146 | Support-Phone: +49 (0) 3641 3101011 (English speaking) 147 | 148 | Administration: 149 | ISPpro Internet KG 150 | Neue Str. 4 151 | D-07639 Bad Klosterlausnitz 152 | GERMANY 153 | 154 | Management...............: Dirk Seidel 155 | Register.................: AG Jena, HRA 202638 156 | VAT Number...............: 162/156/36600 157 | Tax office ..............: Jena 158 | International VAT Number.: DE813856317 159 | ``` 160 | 161 | -------------------------------------------------------------------------------- /README_tc.md: -------------------------------------------------------------------------------- 1 | [English](README.md) | [简体中文](README_sc.md) | 正體中文 2 | 3 | # eu_ex 4 | 5 | eu_ex 是 EUserv_extend 的簡寫。一個 Python 指令碼,可以幫你續期免費 EUserv IPv6 VPS。 6 | 7 | 這個指令碼可以自動檢查你賬戶中的 VPS 數量,如果可以續期,就為 VPS 續期。 8 | 9 | ## 如何使用 10 | 11 | 1. 安裝 Python3 和必要的依賴,以下命令在 debian/ubuntu 為例, 12 | 13 | ```bash 14 | #Install Python3 15 | apt install python3 python3-pip -y 16 | #Intstall dependences 17 | pip install requests beautifulsoup4 18 | ``` 19 | 20 | 2. 不建議在 `main.py` 檔案第 37-38 行直接用你的真實值去替換`USERNAME`和`PASSWORD`。可以使用環境變數傳入。 21 | 22 | 你可以新增多個賬戶,並以一個空格分隔。 23 | 24 | 3. 你可以新增多個 mailparser.io 解析資料的下載 URL ID,並以一個空格分隔。下載的 URL ID 在`https://files.mailparser.io/d/`。 25 | 26 | 4. 將 **Actions secrets** 傳入你的 GitHub Action 執行環境的環境變數。例如,以下環境變數是必需的。 27 | 28 | ``` 29 | env: 30 | USERNAME: ${{ secrets.USERNAME }} 31 | PASSWORD: ${{ secrets.PASSWORD }} 32 | # https://mailparser.io 33 | MAILPARSER_DOWNLOAD_URL_ID: ${{ secrets.MAILPARSER_DOWNLOAD_URL_ID }} 34 | ``` 35 | 36 | ## 郵件轉發和 mailparser 設定 37 | ### 郵件轉發 38 | 39 | 以 gmail 為例, 轉發郵件至 [mailparser](https://mailparser.io)。可以是非 gmail 郵箱,前提是可以收到 euserv 的郵件。目前 outlook/hotmail 是收不到的。 40 | 41 | - ![gmail_filter_keys](./images/gmail_filter_keys.png) 42 | 43 | - ![gmail_filter_setting](./images/gmail_filter_setting.png) 44 | 45 | - ![gmail_forward_setting](./images/gmail_forward_setting.png) 46 | 47 | ### Mailparser 設定 48 | 49 | - 首先建立新的收件箱。 50 | - 建立資料解析規則。 51 | - 資料解析規則,pin 為必需,其他可選。 52 | ![mailparser_data_parsing_rules](./images/mailparser_data_parsing_rules.png) 53 | - pin 的解析規則 54 | ![mailparser_data_parsing_rules_pin](./images/mailparser_data_parsing_rules_pin.png) 55 | - subject 的解析規則 56 | ![mailparser_data_parsing_rules_subject](./images/mailparser_data_parsing_rules_subject.png) 57 | - sender 的解析規則 58 | ![mailparser_data_parsing_rules_sender](./images/mailparser_data_parsing_rules_sender.png) 59 | - receiver 的解析規則 60 | ![mailparser_data_parsing_rules_receiver](./images/mailparser_data_parsing_rules_receiver.png) 61 | - 建立解析資料下載 URL 62 | - 解析資料下載 URL 63 | ![mailparser_parsed_data_downloads](./images/mailparser_parsed_data_downloads.png) 64 | - 解析資料下載設定 65 | ![mailparser_parsed_data_downloads_setting](./images/mailparser_parsed_data_downloads_setting.png) 66 | - mailparser 收件箱設定(可選,為減少收到 spam 郵件的風險,最好設定) 67 | - mailparser 收件箱設定 1 68 | ![mailparser_inbox_setting_1](./images/mailparser_inbox_setting_1.png) 69 | - mailparser 收件箱設定 2 70 | ![mailparser_inbox_setting_2](./images/mailparser_inbox_setting_2.png) 71 | 72 | ## 最終效果 73 | 效果如圖, 74 | 75 | ![mailparser_inbox_setting_2](./images/the_final_effect.png) 76 | 77 | ## 待辦事項 78 | 79 | - [x] ~~驗證 mailparser 解析的`receiver'欄位,以減少惡意郵件的干擾。~~ 由於 mailparser *Inbox Settings - Email Reception*,所以不做了。 80 | - [x] 日誌國際化和本地化。 81 | - [ ] 當驗證碼識別 API 失效時開放預訓練的模型,在本地,不呼叫第三方介面就解決驗證碼識別的問題。 82 | - [ ] 考慮 euserv 訪問超時重試,較低優先順序。 83 | 84 | ## 鳴謝 85 | 86 | - 感謝 EUserv 提供免費的 IPv6 VPS 供我們學習。 87 | - 感謝 CokeMine 和其倉庫貢獻者為我們提供的最初 *EUserv_extend* 指令碼。網際網路永遠不會忘記,但人們會。 88 | 89 | ## FAQs 90 | 91 | 1. **Q**: 可以非 gmail 郵箱嗎? 92 | 93 | **A**: 可以是非 gmail 郵箱,前提是可以收到 euserv 的郵件。目前 outlook/hotmail 是收不到的。 94 | 95 | 2. **Q**: n 個郵箱能在用同一個 mailparser 還是需要申請 n 個 mailparser 與之一 一對應嗎? 96 | 97 | **A**: mailparser free 賬號最多可以設定 10 個收件箱,這 10 個收件箱就可以對應 10 個 euserv 賬號,也就有了 10 個 mailparser parsed data download URL(id)。所以,這得看你 n>10, 還是 n<10 了。n<10, 一個 mailparser 賬號即可,然後 parsed data download URL id 一 一對應 euserv 的註冊郵箱賬號。 98 | 99 | 3. **Q**: 續期指令碼是如何工作的? 100 | 101 | **A**: EUserv 從 2021 年 9 月底開始,設定了第一道門檻,那就是登入出現驗證碼(成功驗證狀態維持 24 小時),所以目前就用 TrueCaptcha 提供的 API(每天都有免費額度) 進行識別。沒過多久,大概是 2021 年11月初,EUserv 又設定了第二道門檻,就是續期時的郵件 PIN 碼驗證,這裡解決辦法就大概兩種:a. 登入郵箱獲取含 EUserv PIN 的郵件。b. 將郵件轉化為 HTTP REST API,自動獲取。這裡採取的是方案 b。方案 b 中可選的存在免費額度的,好像目前僅有 [Mailparser](https://mailparser.io) 和 [Zapier Emails Parser](https://parser.zapier.com/)。方案 b 明顯要優於方案 a。 102 | 103 | ## 參考資訊 104 | 105 | ### EUserv "PIN for the Confirmation of a Security Check" 原始郵件 106 | 107 | ``` 108 | From: EUserv Support 109 | To: xyz@example.com 110 | Subject: EUserv - PIN for the Confirmation of a Security Check 111 | Content-Type: text/plain; charset = utf-8 112 | Dear XYZ, 113 | 114 | you have just requested a PIN for confirmation of a security check at EUserv. If you have not requested the PIN then ignore this email. 115 | 116 | PIN: 117 | 123456 118 | 119 | PLEASE NOTE: If you already have requested a new PIN for the same process this PIN is invalid. Also this PIN is only valid within the session in which it has been requested. This means the PIN is invalid if you for example change the browser or if you logout and perform a new login. 120 | 121 | 122 | Sincerely, 123 | Your customer support EUserv 124 | 125 | -- 126 | Web ................: http://www.euserv.com 127 | Login control panel.: https://support.euserv.com 128 | FAQ ................: http://faq.euserv.com 129 | Help & Guides.......: http://wiki.euserv.com 130 | Community / Forum...: http://forum.euserv.com 131 | Mailing-Liste ......: http://www.euserv.com/en/?show_contact=mailinglist 132 | Twitter ............: http://twitter.com/euservhosting 133 | Facebook ...........: http://www.facebook.com/euservhosting 134 | -- 135 | 136 | EUserv Internet 137 | is a division of 138 | ISPpro Internet KG 139 | 140 | Postal address: 141 | ISPpro Internet KG 142 | Division EUserv Internet 143 | P.O. Box 2224 144 | 07622 Hermsdorf 145 | GERMANY 146 | 147 | Support-Phone: +49 (0) 3641 3101011 (English speaking) 148 | 149 | Administration: 150 | ISPpro Internet KG 151 | Neue Str. 4 152 | D-07639 Bad Klosterlausnitz 153 | GERMANY 154 | 155 | Management...............: Dirk Seidel 156 | Register.................: AG Jena, HRA 202638 157 | VAT Number...............: 162/156/36600 158 | Tax office ..............: Jena 159 | International VAT Number.: DE813856317 160 | ``` 161 | 162 | -------------------------------------------------------------------------------- /images/gmail_filter_keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fscarmen2/eu_ex/0b28753133f75c595815342a4f3c47db501d7cdb/images/gmail_filter_keys.png -------------------------------------------------------------------------------- /images/gmail_filter_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fscarmen2/eu_ex/0b28753133f75c595815342a4f3c47db501d7cdb/images/gmail_filter_setting.png -------------------------------------------------------------------------------- /images/gmail_forward_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fscarmen2/eu_ex/0b28753133f75c595815342a4f3c47db501d7cdb/images/gmail_forward_setting.png -------------------------------------------------------------------------------- /images/mailparser_data_parsing_rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fscarmen2/eu_ex/0b28753133f75c595815342a4f3c47db501d7cdb/images/mailparser_data_parsing_rules.png -------------------------------------------------------------------------------- /images/mailparser_data_parsing_rules_pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fscarmen2/eu_ex/0b28753133f75c595815342a4f3c47db501d7cdb/images/mailparser_data_parsing_rules_pin.png -------------------------------------------------------------------------------- /images/mailparser_data_parsing_rules_receiver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fscarmen2/eu_ex/0b28753133f75c595815342a4f3c47db501d7cdb/images/mailparser_data_parsing_rules_receiver.png -------------------------------------------------------------------------------- /images/mailparser_data_parsing_rules_sender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fscarmen2/eu_ex/0b28753133f75c595815342a4f3c47db501d7cdb/images/mailparser_data_parsing_rules_sender.png -------------------------------------------------------------------------------- /images/mailparser_data_parsing_rules_subject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fscarmen2/eu_ex/0b28753133f75c595815342a4f3c47db501d7cdb/images/mailparser_data_parsing_rules_subject.png -------------------------------------------------------------------------------- /images/mailparser_inbox_setting_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fscarmen2/eu_ex/0b28753133f75c595815342a4f3c47db501d7cdb/images/mailparser_inbox_setting_1.png -------------------------------------------------------------------------------- /images/mailparser_inbox_setting_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fscarmen2/eu_ex/0b28753133f75c595815342a4f3c47db501d7cdb/images/mailparser_inbox_setting_2.png -------------------------------------------------------------------------------- /images/mailparser_parsed_data_downloads.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fscarmen2/eu_ex/0b28753133f75c595815342a4f3c47db501d7cdb/images/mailparser_parsed_data_downloads.png -------------------------------------------------------------------------------- /images/mailparser_parsed_data_downloads_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fscarmen2/eu_ex/0b28753133f75c595815342a4f3c47db501d7cdb/images/mailparser_parsed_data_downloads_setting.png -------------------------------------------------------------------------------- /images/the_final_effect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fscarmen2/eu_ex/0b28753133f75c595815342a4f3c47db501d7cdb/images/the_final_effect.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # 4 | # SPDX-FileCopyrightText: (c) 2020-2021 CokeMine & Its repository contributors 5 | # SPDX-FileCopyrightText: (c) 2021-2022 A beam of light 6 | # 7 | # SPDX-License-Identifier: GPL-3.0-or-later 8 | # 9 | 10 | """ 11 | euserv auto-renew script 12 | 13 | ChangeLog 14 | 15 | v2021.12.15 16 | - Implemented a simple localization system, log output localization 17 | - Reformat code via black 18 | 19 | v2021.11.26 20 | - Handle TrueCaptcha service exception 21 | - Adjust TrueCaptcha constraint parameters for high availability. 22 | Plus, the CAPTCHA of EUserv is currently case-insensitive, so the above adjustment works. 23 | 24 | v2021.11.06 25 | - Receive renew PIN(6-digits) using mailparser parsed data download url 26 | workflow: auto-forward your EUserv PIN email to your mailparser inbox 27 | -> parsing PIN via mailparser -> get PIN from mailparser 28 | - Update kc2_security_password_get_token request 29 | 30 | v2021.09.30 31 | - Captcha automatic recognition using TrueCaptcha API 32 | - Email notification 33 | - Add login failure retry mechanism 34 | - reformat log info 35 | 36 | """ 37 | 38 | import os 39 | import re 40 | import json 41 | import time 42 | import base64 43 | 44 | from email.mime.application import MIMEApplication 45 | from email.mime.multipart import MIMEMultipart 46 | from email.mime.text import MIMEText 47 | from smtplib import SMTP_SSL, SMTPDataError 48 | 49 | import requests 50 | from bs4 import BeautifulSoup 51 | 52 | # Please use one space to separate multiple accounts 53 | # euserv username or email 54 | USERNAME = os.environ["USERNAME"] 55 | # euserv password 56 | PASSWORD = os.environ["PASSWORD"] 57 | 58 | # default value is TrueCaptcha demo credential, 59 | # you can use your own credential via set environment variables: 60 | # TRUECAPTCHA_USERID and TRUECAPTCHA_APIKEY 61 | # demo: https://apitruecaptcha.org/demo 62 | # demo2: https://apitruecaptcha.org/demo2 63 | # demo apikey also has a limit of 100 times per day 64 | # { 65 | # 'error': '101.0 above free usage limit 100 per day and no balance', 66 | # 'requestId': '7690c065-70e0-4757-839b-5fd8381e65c7' 67 | # } 68 | TRUECAPTCHA_USERID = os.environ.get("TRUECAPTCHA_USERID", "arun56") 69 | TRUECAPTCHA_APIKEY = os.environ.get("TRUECAPTCHA_APIKEY", "wMjXmBIcHcdYqO2RrsVN") 70 | 71 | # Extract key data from your emails, automatically. https://mailparser.io 72 | # 30 Emails/Month, 10 inboxes and unlimited downloads for free. 73 | # Please use one space to separate multiple mailparser download link ids, 74 | # in order to correspond to the EUserv account/email. 75 | MAILPARSER_DOWNLOAD_URL_ID = os.environ["MAILPARSER_DOWNLOAD_URL_ID"] 76 | # mailparser parsed data download base url 77 | MAILPARSER_DOWNLOAD_BASE_URL = "https://files.mailparser.io/d/" 78 | 79 | # Telegram Bot Push https://core.telegram.org/bots/api#authorizing-your-bot 80 | # Obtained via @BotFather application, for example: 1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw 81 | TG_BOT_TOKEN = "" 82 | # User, group or channel ID, for example: 129xxx206 83 | TG_USER_ID = "" 84 | # Build your own API reverse proxy address for use when the network environment is inaccessible, 85 | # and keep the default if the network is normal. 86 | TG_API_HOST = "https://api.telegram.org" 87 | 88 | # Email notification via yandex service, you can modify yourself to use other email service notifications. 89 | RECEIVER_EMAIL = os.environ.get("RECEIVER_EMAIL", "") 90 | YD_EMAIL = os.environ.get("YD_EMAIL", "") 91 | # yandex mail using third party APP authorization code 92 | YD_APP_PWD = os.environ.get("YD_APP_PWD", "") 93 | 94 | # Server Chan(Server 酱, name in Chinese) https://sct.ftqq.com 95 | # Free quota: up to 5 messages per day, cards show only title, maximum 1000 API requests per day, up to 5 messages per minute. 96 | # Server Chan SENDKEY, no need to push can be ignored 97 | SERVER_CHAN_SENDKEY = os.environ.get("SERVER_CHAN_SENDKEY", "") 98 | 99 | # Magic internet access 100 | PROXIES = {"http": "http://127.0.0.1:10808", "https": "http://127.0.0.1:10808"} 101 | 102 | # Maximum number of login retry 103 | LOGIN_MAX_RETRY_COUNT = 5 104 | 105 | # Waiting time of receiving PIN, units are seconds. 106 | WAITING_TIME_OF_PIN = 15 107 | 108 | # Checking CAPTCHA API usage, options: True or False 109 | CHECK_CAPTCHA_SOLVER_USAGE = True 110 | 111 | user_agent = ( 112 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " 113 | "Chrome/96.0.4664.110 Safari/537.36" 114 | ) 115 | 116 | # Blank 117 | desp = "" 118 | 119 | # Simplified Chinese Translation 120 | chs_locale = { 121 | ":": ":", 122 | ",": ",", 123 | ".": "。", 124 | "!": "!", 125 | "...": "......", 126 | "~": "~", 127 | "Login retried the @@@ time": "登录重试第 @@@ 次", 128 | "You are using the demo apikey": "你正在使用演示版 apikey", 129 | "There is no guarantee that demo apikey will work in the future": "无法保证演示版 apikey 在将来也能使用", 130 | "You are using your own apikey": " 你正在使用自己的 apikey", 131 | "Service Exception": "服务异常", 132 | "Returned JSON": "返回的 JSON", 133 | "Failed to find parsed results": "找不到解析结果", 134 | "Performing CAPTCHA recognition": "进行验证码识别", 135 | "The recognized CAPTCHA is": "识别的验证码是", 136 | "current date": "当前日期", 137 | "api usage count": "api 使用次数", 138 | "CAPTCHA Verification passed": "CAPTCHA 验证通过", 139 | "CAPTCHA Verification failed": "CAPTCHA 验证失败", 140 | "PIN": "PIN", 141 | "ServerID": "服务器 ID", 142 | "Renew Failed": "续期失败", 143 | "ALL Work Done": "所有工作都已完成", 144 | "Enjoy": "使用愉快", 145 | "EUserv Renewal Logs": "EUserv 续期日志", 146 | "push failed": "推送失败", 147 | "push successfully": "推送成功", 148 | "Server Chan": "Server 酱", 149 | "Checking": "正在检查", 150 | "You have not added any accounts": "你没有添加任何账户", 151 | "The number of usernames and passwords do not match": "用户名和密码的数量不匹配", 152 | "The number of mailparser_dl_url_ids and usernames do not match": "mailparser 下载链接 id 和用户名的数量不匹配", 153 | "Renewing the @@@ account": "正在续期第 @@@ 个账号", 154 | "The @@@ account login failed": "第 @@@ 个账号登录失败", 155 | "please check the login information": "请检查登录信息", 156 | "renewals are being attempted": "正在尝试续期", 157 | "The @@@ account is detected": "检测到第 @@@ 个账号", 158 | "with @@@ VPS": "有 @@@ 台 VPS", 159 | "renew Error": "续期错误", 160 | "has been successfully renewed": "已成功续期", 161 | "does not need to be renewed": "不需要续期", 162 | } 163 | 164 | # Traditional Chinese Translation 165 | cht_locale = { 166 | ":": ":", 167 | ",": ",", 168 | ".": "。", 169 | "!": "!", 170 | "...": "......", 171 | "~": "~", 172 | "Login retried the @@@ time": "登錄重試第 @@@ 次", 173 | "You are using the demo apikey": "你正在使用演示版 apikey", 174 | "There is no guarantee that demo apikey will work in the future": "無法保證演示版 apikey 在將來也能使用", 175 | "You are using your own apikey": " 你正在使用你自己的 apikey", 176 | "Service Exception": "服務異常", 177 | "Returned JSON": "返回的 JSON", 178 | "Failed to find parsed results": "找不到解析結果", 179 | "Performing CAPTCHA recognition": "進行驗證碼識別", 180 | "The recognized CAPTCHA is": "識別的驗證碼是", 181 | "current date": "當前日期", 182 | "api usage count": "api 已使用次數", 183 | "CAPTCHA Verification passed": "CAPTCHA 驗證通過", 184 | "CAPTCHA Verification failed": "CAPTCHA 驗證失敗", 185 | "PIN": "PIN", 186 | "ServerID": "伺服器 ID", 187 | "Renew Failed": "續期失敗", 188 | "ALL Work Done": "所有工作都已完成", 189 | "Enjoy": "使用愉快", 190 | "EUserv Renewal Logs": "EUserv 續期日誌", 191 | "push failed": "推送失敗", 192 | "push successfully": "推送成功", 193 | "Server Chan": "Server 醬", 194 | "Checking": "正在檢查", 195 | "You have not added any accounts": "你沒有新增任何賬戶", 196 | "The number of usernames and passwords do not match": "使用者名稱和密碼的數量不匹配", 197 | "The number of mailparser_dl_url_ids and usernames do not match": "mailparser 下載連結 id 和使用者名稱的數量不匹配", 198 | "Renewing the @@@ account": "正在續期第 @@@ 個賬號", 199 | "The @@@ account login failed": "第 @@@ 個賬號登入失敗", 200 | "please check the login information": "請檢查登入資訊", 201 | "renewals are being attempted": "正在嘗試續期", 202 | "The @@@ account is detected": "檢測到第 @@@ 個賬號", 203 | "with @@@ VPS": "有 @@@ 臺 VPS", 204 | "renew Error": "續期錯誤", 205 | "has been successfully renewed": "已成功續期", 206 | "does not need to be renewed": "不需要續期", 207 | } 208 | 209 | # Localization 210 | log_lang_options = { 211 | "en": lambda x: x, 212 | "chs": lambda x: chs_locale.get(x, x), 213 | "cht": lambda x: cht_locale.get(x, x), 214 | } 215 | 216 | # Language Options: en/chs/cht, or leave it blank 217 | log_lang = "chs" 218 | 219 | ordinal = lambda n: "{}{}".format( 220 | n, 221 | "tsnrhtdd"[(n / 10 % 10 != 1) * (n % 10 < 4) * n % 10 :: 4], 222 | ) 223 | 224 | 225 | def log(info: str): 226 | print(info) 227 | global desp 228 | desp = desp + info + "\n\n" 229 | 230 | 231 | def login_retry(*args, **kwargs): 232 | def wrapper(func): 233 | def inner(username, password): 234 | ret, ret_session = func(username, password) 235 | max_retry = kwargs.get("max_retry") 236 | # default retry 3 times 237 | if not max_retry: 238 | max_retry = 3 239 | number = 0 240 | if ret == "-1": 241 | while number < max_retry: 242 | number += 1 243 | if number > 1: 244 | log( 245 | "[EUserv] {} {}".format( 246 | log_lang_options.get(log_lang, lambda x: x)( 247 | "Login retried the @@@ time" 248 | ).replace("@@@", ordinal(number)), 249 | log_lang_options.get(log_lang, lambda x: x)("."), 250 | ) 251 | ) 252 | sess_id, session = func(username, password) 253 | if sess_id != "-1": 254 | return sess_id, session 255 | else: 256 | if number == max_retry: 257 | return sess_id, session 258 | else: 259 | return ret, ret_session 260 | 261 | return inner 262 | 263 | return wrapper 264 | 265 | 266 | def captcha_solver(captcha_image_url: str, session: requests.session) -> dict: 267 | """ 268 | TrueCaptcha API doc: https://apitruecaptcha.org/api 269 | Free to use 100 requests per day. 270 | -- response:: 271 | { 272 | "result": "", ==> Or "result": 0 273 | "conf": 0.85, 274 | "usage": 0, 275 | "requestId": "ed0006e5-69f0-4617-b698-97dc054f9022", 276 | "version": "dev2" 277 | } 278 | """ 279 | response = session.get(captcha_image_url) 280 | encoded_string = base64.b64encode(response.content) 281 | url = "https://api.apitruecaptcha.org/one/gettext" 282 | 283 | # Since "case": "mixed", "mode": "human" 284 | # can sometimes cause internal errors in the truecaptcha server. 285 | # So a more relaxed constraint(lower/upper & default) is used here. 286 | # Plus, the CAPTCHA of EUserv is currently case-insensitive, so the below adjustment works. 287 | data = { 288 | "userid": TRUECAPTCHA_USERID, 289 | "apikey": TRUECAPTCHA_APIKEY, 290 | # case sensitivity of text (upper | lower| mixed) 291 | "case": "lower", 292 | # use human or AI (human | default) 293 | "mode": "default", 294 | "data": str(encoded_string)[2:-1], 295 | } 296 | r = requests.post(url=url, json=data) 297 | j = json.loads(r.text) 298 | return j 299 | 300 | 301 | def handle_captcha_solved_result(solved: dict) -> str: 302 | """Since CAPTCHA sometimes appears as a very simple binary arithmetic expression. 303 | But since recognition sometimes doesn't show the result of the calculation directly, 304 | that's what this function is for. 305 | """ 306 | if "result" in solved: 307 | solved_result = solved["result"] 308 | if isinstance(solved_result, str): 309 | if "RESULT IS" in solved_result: 310 | log( 311 | "[Captcha Solver] {}{}".format( 312 | log_lang_options.get(log_lang, lambda x: x)( 313 | "You are using the demo apikey" 314 | ), 315 | log_lang_options.get(log_lang, lambda x: x)("."), 316 | ) 317 | ) 318 | print( 319 | "{}{}".format( 320 | log_lang_options.get(log_lang, lambda x: x)( 321 | "There is no guarantee that demo apikey will work in the future" 322 | ), 323 | log_lang_options.get(log_lang, lambda x: x)("!"), 324 | ) 325 | ) 326 | # because using demo apikey 327 | text = re.findall(r"RESULT IS . (.*) .", solved_result)[0] 328 | else: 329 | # using your own apikey 330 | log( 331 | "[Captcha Solver] {}{}".format( 332 | log_lang_options.get(log_lang, lambda x: x)( 333 | "You are using your own apikey" 334 | ), 335 | log_lang_options.get(log_lang, lambda x: x)("."), 336 | ) 337 | ) 338 | text = solved_result 339 | operators = ["X", "x", "+", "-"] 340 | if any(x in text for x in operators): 341 | for operator in operators: 342 | operator_pos = text.find(operator) 343 | if operator == "x" or operator == "X": 344 | operator = "*" 345 | if operator_pos != -1: 346 | left_part = text[:operator_pos] 347 | right_part = text[operator_pos + 1 :] 348 | if left_part.isdigit() and right_part.isdigit(): 349 | return eval( 350 | "{left} {operator} {right}".format( 351 | left=left_part, operator=operator, right=right_part 352 | ) 353 | ) 354 | else: 355 | # Because these symbols("X", "x", "+", "-") do not appear at the same time, 356 | # it just contains an arithmetic symbol. 357 | return text 358 | else: 359 | return text 360 | else: 361 | print( 362 | "[Captcha Solver] {}{} {}".format( 363 | log_lang_options.get(log_lang, lambda x: x)("Returned JSON"), 364 | log_lang_options.get(log_lang, lambda x: x)(":"), 365 | solved, 366 | ) 367 | ) 368 | log( 369 | "[Captcha Solver] {}{}".format( 370 | log_lang_options.get(log_lang, lambda x: x)("Service Exception"), 371 | log_lang_options.get(log_lang, lambda x: x)("!"), 372 | ) 373 | ) 374 | raise ValueError("[Captcha Solver] Service Exception!") 375 | else: 376 | print( 377 | "[Captcha Solver] {}{} {}".format( 378 | log_lang_options.get(log_lang, lambda x: x)("Returned JSON"), 379 | log_lang_options.get(log_lang, lambda x: x)(":"), 380 | solved, 381 | ) 382 | ) 383 | log( 384 | "[Captcha Solver] {}{}".format( 385 | log_lang_options.get(log_lang, lambda x: x)( 386 | "Failed to find parsed results" 387 | ), 388 | log_lang_options.get(log_lang, lambda x: x)("!"), 389 | ) 390 | ) 391 | raise KeyError("[Captcha Solver] Failed to find parsed results!") 392 | 393 | 394 | def get_captcha_solver_usage() -> dict: 395 | url = "https://api.apitruecaptcha.org/one/getusage" 396 | 397 | params = { 398 | "username": TRUECAPTCHA_USERID, 399 | "apikey": TRUECAPTCHA_APIKEY, 400 | } 401 | r = requests.get(url=url, params=params) 402 | j = json.loads(r.text) 403 | return j 404 | 405 | 406 | def get_pin_from_mailparser(url_id: str) -> str: 407 | """ 408 | response format: 409 | [ 410 | { 411 | "id": "83b95f50f6202fb03950afbe00975eab", 412 | "received_at": "2021-11-06 02:30:07", ==> up to mailparser account timezone setting, here is UTC 0000. 413 | "processed_at": "2021-11-06 02:30:07", 414 | "pin": "123456" 415 | } 416 | ] 417 | """ 418 | response = requests.get( 419 | f"{MAILPARSER_DOWNLOAD_BASE_URL}{url_id}", 420 | # Mailparser parsed data download using Basic Authentication. 421 | # auth=("", "") 422 | ) 423 | pin = response.json()[0]["pin"] 424 | return pin 425 | 426 | 427 | @login_retry(max_retry=LOGIN_MAX_RETRY_COUNT) 428 | def login(username: str, password: str) -> (str, requests.session): 429 | headers = {"user-agent": user_agent, "origin": "https://www.euserv.com"} 430 | url = "https://support.euserv.com/index.iphp" 431 | captcha_image_url = "https://support.euserv.com/securimage_show.php" 432 | session = requests.Session() 433 | 434 | sess = session.get(url, headers=headers) 435 | sess_id = re.findall("PHPSESSID=(\\w{10,100});", str(sess.headers))[0] 436 | # visit png 437 | logo_png_url = "https://support.euserv.com/pic/logo_small.png" 438 | session.get(logo_png_url, headers=headers) 439 | 440 | login_data = { 441 | "email": username, 442 | "password": password, 443 | "form_selected_language": "en", 444 | "Submit": "Login", 445 | "subaction": "login", 446 | "sess_id": sess_id, 447 | } 448 | f = session.post(url, headers=headers, data=login_data) 449 | f.raise_for_status() 450 | 451 | if ( 452 | f.text.find("Hello") == -1 453 | and f.text.find("Confirm or change your customer data here") == -1 454 | ): 455 | if ( 456 | f.text.find( 457 | "To finish the login process please solve the following captcha." 458 | ) 459 | == -1 460 | ): 461 | return "-1", session 462 | else: 463 | log( 464 | "[Captcha Solver] {}{}".format( 465 | log_lang_options.get(log_lang, lambda x: x)( 466 | "Performing CAPTCHA recognition" 467 | ), 468 | log_lang_options.get(log_lang, lambda x: x)("..."), 469 | ) 470 | ) 471 | solved_result = captcha_solver(captcha_image_url, session) 472 | try: 473 | captcha_code = handle_captcha_solved_result(solved_result) 474 | log( 475 | "[Captcha Solver] {}{} {}".format( 476 | log_lang_options.get(log_lang, lambda x: x)( 477 | "The recognized CAPTCHA is" 478 | ), 479 | log_lang_options.get(log_lang, lambda x: x)(":"), 480 | captcha_code, 481 | ) 482 | ) 483 | 484 | if CHECK_CAPTCHA_SOLVER_USAGE: 485 | usage = get_captcha_solver_usage() 486 | log( 487 | "[Captcha Solver] {} {} {}{} {}".format( 488 | log_lang_options.get(log_lang, lambda x: x)("current date"), 489 | usage[0]["date"], 490 | log_lang_options.get(log_lang, lambda x: x)( 491 | "api usage count" 492 | ), 493 | log_lang_options.get(log_lang, lambda x: x)(":"), 494 | usage[0]["count"], 495 | ) 496 | ) 497 | 498 | f2 = session.post( 499 | url, 500 | headers=headers, 501 | data={ 502 | "subaction": "login", 503 | "sess_id": sess_id, 504 | "captcha_code": captcha_code, 505 | }, 506 | ) 507 | if ( 508 | f2.text.find( 509 | "To finish the login process please solve the following captcha." 510 | ) 511 | == -1 512 | ): 513 | log( 514 | "[Captcha Solver] {}".format( 515 | log_lang_options.get(log_lang, lambda x: x)( 516 | "CAPTCHA Verification passed" 517 | ) 518 | ) 519 | ) 520 | return sess_id, session 521 | else: 522 | log( 523 | "[Captcha Solver] {}".format( 524 | log_lang_options.get(log_lang, lambda x: x)( 525 | "CAPTCHA Verification failed" 526 | ) 527 | ) 528 | ) 529 | return "-1", session 530 | except (KeyError, ValueError): 531 | return "-1", session 532 | else: 533 | return sess_id, session 534 | 535 | 536 | def get_servers(sess_id: str, session: requests.session) -> {}: 537 | d = {} 538 | url = "https://support.euserv.com/index.iphp?sess_id=" + sess_id 539 | headers = {"user-agent": user_agent, "origin": "https://www.euserv.com"} 540 | f = session.get(url=url, headers=headers) 541 | f.raise_for_status() 542 | soup = BeautifulSoup(f.text, "html.parser") 543 | for tr in soup.select( 544 | "#kc2_order_customer_orders_tab_content_1 .kc2_order_table.kc2_content_table tr" 545 | ): 546 | server_id = tr.select(".td-z1-sp1-kc") 547 | if not len(server_id) == 1: 548 | continue 549 | flag = ( 550 | True 551 | if tr.select(".td-z1-sp2-kc .kc2_order_action_container")[0] 552 | .get_text() 553 | .find("Contract extension possible from") 554 | == -1 555 | else False 556 | ) 557 | d[server_id[0].get_text()] = flag 558 | return d 559 | 560 | 561 | def renew( 562 | sess_id: str, 563 | session: requests.session, 564 | password: str, 565 | order_id: str, 566 | mailparser_dl_url_id: str, 567 | ) -> bool: 568 | url = "https://support.euserv.com/index.iphp" 569 | headers = { 570 | "user-agent": user_agent, 571 | "Host": "support.euserv.com", 572 | "origin": "https://support.euserv.com", 573 | "Referer": "https://support.euserv.com/index.iphp", 574 | } 575 | data = { 576 | "Submit": "Extend contract", 577 | "sess_id": sess_id, 578 | "ord_no": order_id, 579 | "subaction": "choose_order", 580 | "choose_order_subaction": "show_contract_details", 581 | } 582 | session.post(url, headers=headers, data=data) 583 | 584 | # pop up 'Security Check' window, it will trigger 'send PIN' automatically. 585 | session.post( 586 | url, 587 | headers=headers, 588 | data={ 589 | "sess_id": sess_id, 590 | "subaction": "show_kc2_security_password_dialog", 591 | "prefix": "kc2_customer_contract_details_extend_contract_", 592 | "type": "1", 593 | }, 594 | ) 595 | 596 | # # trigger 'Send new PIN to your Email-Address' (optional), 597 | # new_pin = session.post(url, headers=headers, data={ 598 | # "sess_id": sess_id, 599 | # "subaction": "kc2_security_password_send_pin", 600 | # "ident": f"kc2_customer_contract_details_extend_contract_{order_id}" 601 | # }) 602 | # if not json.loads(new_pin.text)["rc"] == "100": 603 | # print("new PIN Not Sended") 604 | # return False 605 | 606 | # sleep WAITING_TIME_OF_PIN seconds waiting for mailparser email parsed PIN 607 | time.sleep(WAITING_TIME_OF_PIN) 608 | pin = get_pin_from_mailparser(mailparser_dl_url_id) 609 | log( 610 | "[MailParser] {}{} {}".format( 611 | log_lang_options.get(log_lang, lambda x: x)("PIN"), 612 | log_lang_options.get(log_lang, lambda x: x)(":"), 613 | pin, 614 | ) 615 | ) 616 | 617 | # using PIN instead of password to get token 618 | data = { 619 | "auth": pin, 620 | "sess_id": sess_id, 621 | "subaction": "kc2_security_password_get_token", 622 | "prefix": "kc2_customer_contract_details_extend_contract_", 623 | "type": 1, 624 | "ident": f"kc2_customer_contract_details_extend_contract_{order_id}", 625 | } 626 | f = session.post(url, headers=headers, data=data) 627 | f.raise_for_status() 628 | if not json.loads(f.text)["rs"] == "success": 629 | return False 630 | token = json.loads(f.text)["token"]["value"] 631 | data = { 632 | "sess_id": sess_id, 633 | "ord_id": order_id, 634 | "subaction": "kc2_customer_contract_details_extend_contract_term", 635 | "token": token, 636 | } 637 | session.post(url, headers=headers, data=data) 638 | time.sleep(5) 639 | return True 640 | 641 | 642 | def check(sess_id: str, session: requests.session): 643 | print( 644 | "{}{}".format( 645 | log_lang_options.get(log_lang, lambda x: x)("Checking"), 646 | log_lang_options.get(log_lang, lambda x: x)("..."), 647 | ) 648 | ) 649 | d = get_servers(sess_id, session) 650 | flag = True 651 | for key, val in d.items(): 652 | if val: 653 | flag = False 654 | log( 655 | "[EUserv] {}{} {} {}{}".format( 656 | log_lang_options.get(log_lang, lambda x: x)("ServerID"), 657 | log_lang_options.get(log_lang, lambda x: x)(":"), 658 | key, 659 | log_lang_options.get(log_lang, lambda x: x)("Renew Failed"), 660 | log_lang_options.get(log_lang, lambda x: x)("!"), 661 | ) 662 | ) 663 | 664 | if flag: 665 | log( 666 | "[EUserv] {}{} {}{}".format( 667 | log_lang_options.get(log_lang, lambda x: x)("ALL Work Done"), 668 | log_lang_options.get(log_lang, lambda x: x)("!"), 669 | log_lang_options.get(log_lang, lambda x: x)("Enjoy"), 670 | log_lang_options.get(log_lang, lambda x: x)("~"), 671 | ) 672 | ) 673 | 674 | 675 | # Telegram Bot Push https://core.telegram.org/bots/api#authorizing-your-bot 676 | def telegram(): 677 | data = ( 678 | ("chat_id", TG_USER_ID), 679 | ( 680 | "text", 681 | "{}\n\n".format( 682 | log_lang_options.get(log_lang, lambda x: x)("EUserv Renewal Logs") 683 | ) 684 | + desp, 685 | ), 686 | ) 687 | response = requests.post( 688 | TG_API_HOST + "/bot" + TG_BOT_TOKEN + "/sendMessage", data=data 689 | ) 690 | if response.status_code != 200: 691 | print( 692 | "Telegram Bot {}".format( 693 | log_lang_options.get(log_lang, lambda x: x)("push failed") 694 | ) 695 | ) 696 | else: 697 | print( 698 | "Telegram Bot {}".format( 699 | log_lang_options.get(log_lang, lambda x: x)("push successfully") 700 | ) 701 | ) 702 | 703 | 704 | # Yandex mail notification 705 | def send_mail_by_yandex( 706 | to_email, from_email, subject, text, files, sender_email, sender_password 707 | ): 708 | msg = MIMEMultipart() 709 | msg["Subject"] = subject 710 | msg["From"] = from_email 711 | msg["To"] = to_email 712 | msg.attach(MIMEText(text, _charset="utf-8")) 713 | if files is not None: 714 | for file in files: 715 | file_name, file_content = file 716 | # print(file_name) 717 | part = MIMEApplication(file_content) 718 | part.add_header( 719 | "Content-Disposition", "attachment", filename=("gb18030", "", file_name) 720 | ) 721 | msg.attach(part) 722 | s = SMTP_SSL("smtp.yandex.ru", 465) 723 | s.login(sender_email, sender_password) 724 | try: 725 | s.sendmail(msg["From"], msg["To"], msg.as_string()) 726 | except SMTPDataError as e: 727 | raise e 728 | finally: 729 | s.close() 730 | 731 | 732 | # eMail push 733 | def email(): 734 | msg = ( 735 | "{}\n\n".format( 736 | log_lang_options.get(log_lang, lambda x: x)("EUserv Renewal Logs") 737 | ) 738 | + desp 739 | ) 740 | try: 741 | send_mail_by_yandex( 742 | RECEIVER_EMAIL, 743 | YD_EMAIL, 744 | log_lang_options.get(log_lang, lambda x: x)("EUserv Renewal Logs"), 745 | msg, 746 | None, 747 | YD_EMAIL, 748 | YD_APP_PWD, 749 | ) 750 | print( 751 | "eMail {}".format( 752 | log_lang_options.get(log_lang, lambda x: x)("push successfully") 753 | ) 754 | ) 755 | except requests.exceptions.RequestException as e: 756 | print(str(e)) 757 | print( 758 | "eMail {}".format( 759 | log_lang_options.get(log_lang, lambda x: x)("push failed") 760 | ) 761 | ) 762 | except SMTPDataError as e1: 763 | print(str(e1)) 764 | print( 765 | "eMail {}".format( 766 | log_lang_options.get(log_lang, lambda x: x)("push failed") 767 | ) 768 | ) 769 | 770 | 771 | # Server Chan https://sct.ftqq.com 772 | def server_chan(): 773 | data = { 774 | "title": log_lang_options.get(log_lang, lambda x: x)("EUserv Renewal Logs"), 775 | "desp": desp, 776 | } 777 | response = requests.post( 778 | f"https://sctapi.ftqq.com/{SERVER_CHAN_SENDKEY}.send", data=data 779 | ) 780 | if response.status_code != 200: 781 | print( 782 | "{} {}".format( 783 | log_lang_options.get(log_lang, lambda x: x)("Server Chan"), 784 | log_lang_options.get(log_lang, lambda x: x)("push failed"), 785 | ) 786 | ) 787 | else: 788 | print( 789 | "{} {}".format( 790 | log_lang_options.get(log_lang, lambda x: x)("Server Chan"), 791 | log_lang_options.get(log_lang, lambda x: x)("push successfully"), 792 | ) 793 | ) 794 | 795 | 796 | if __name__ == "__main__": 797 | if not USERNAME or not PASSWORD: 798 | log( 799 | "[EUserv] {}{}".format( 800 | log_lang_options.get(log_lang, lambda x: x)( 801 | "You have not added any accounts" 802 | ), 803 | log_lang_options.get(log_lang, lambda x: x)("."), 804 | ) 805 | ) 806 | exit(1) 807 | user_list = USERNAME.strip().split() 808 | passwd_list = PASSWORD.strip().split() 809 | mailparser_dl_url_id_list = MAILPARSER_DOWNLOAD_URL_ID.strip().split() 810 | if len(user_list) != len(passwd_list): 811 | log( 812 | "[EUserv] {}{}".format( 813 | log_lang_options.get(log_lang, lambda x: x)( 814 | "The number of usernames and passwords do not match" 815 | ), 816 | log_lang_options.get(log_lang, lambda x: x)("!"), 817 | ) 818 | ) 819 | exit(1) 820 | if len(mailparser_dl_url_id_list) != len(user_list): 821 | log( 822 | "[Mailparser] {}{}".format( 823 | log_lang_options.get(log_lang, lambda x: x)( 824 | "The number of mailparser_dl_url_ids and usernames do not match" 825 | ), 826 | log_lang_options.get(log_lang, lambda x: x)("!"), 827 | ) 828 | ) 829 | exit(1) 830 | for i in range(len(user_list)): 831 | print("*" * 30) 832 | log( 833 | "[EUserv] {}{}".format( 834 | log_lang_options.get(log_lang, lambda x: x)( 835 | "Renewing the @@@ account" 836 | ).replace("@@@", ordinal(i + 1)), 837 | log_lang_options.get(log_lang, lambda x: x)("..."), 838 | ) 839 | ) 840 | sessid, s = login(user_list[i], passwd_list[i]) 841 | if sessid == "-1": 842 | log( 843 | "[EUserv] {}{} {}{}".format( 844 | log_lang_options.get(log_lang, lambda x: x)( 845 | "The @@@ account login failed" 846 | ).replace("@@@", ordinal(i + 1)), 847 | log_lang_options.get(log_lang, lambda x: x)(","), 848 | log_lang_options.get(log_lang, lambda x: x)( 849 | "please check the login information" 850 | ), 851 | log_lang_options.get(log_lang, lambda x: x)("."), 852 | ) 853 | ) 854 | continue 855 | SERVERS = get_servers(sessid, s) 856 | log( 857 | "[EUserv] {} {}{} {}{}".format( 858 | log_lang_options.get(log_lang, lambda x: x)( 859 | "The @@@ account is detected" 860 | ).replace("@@@", ordinal(i + 1)), 861 | log_lang_options.get(log_lang, lambda x: x)("with @@@ VPS").replace( 862 | "@@@", str(len(SERVERS)) 863 | ), 864 | log_lang_options.get(log_lang, lambda x: x)(","), 865 | log_lang_options.get(log_lang, lambda x: x)( 866 | "renewals are being attempted" 867 | ), 868 | log_lang_options.get(log_lang, lambda x: x)("..."), 869 | ) 870 | ) 871 | for k, v in SERVERS.items(): 872 | if v: 873 | if not renew( 874 | sessid, s, passwd_list[i], k, mailparser_dl_url_id_list[i] 875 | ): 876 | log( 877 | "[EUserv] {}{} {} {}{}".format( 878 | log_lang_options.get(log_lang, lambda x: x)("ServerID"), 879 | log_lang_options.get(log_lang, lambda x: x)(":"), 880 | k, 881 | log_lang_options.get(log_lang, lambda x: x)("renew Error"), 882 | log_lang_options.get(log_lang, lambda x: x)("!"), 883 | ) 884 | ) 885 | else: 886 | log( 887 | "[EUserv] {}{} {} {}{}".format( 888 | log_lang_options.get(log_lang, lambda x: x)("ServerID"), 889 | log_lang_options.get(log_lang, lambda x: x)(":"), 890 | k, 891 | log_lang_options.get(log_lang, lambda x: x)( 892 | "has been successfully renewed" 893 | ), 894 | log_lang_options.get(log_lang, lambda x: x)("!"), 895 | ) 896 | ) 897 | else: 898 | log( 899 | "[EUserv] {}{} {} {}{}".format( 900 | log_lang_options.get(log_lang, lambda x: x)("ServerID"), 901 | log_lang_options.get(log_lang, lambda x: x)(":"), 902 | k, 903 | log_lang_options.get(log_lang, lambda x: x)( 904 | "does not need to be renewed" 905 | ), 906 | log_lang_options.get(log_lang, lambda x: x)("."), 907 | ) 908 | ) 909 | time.sleep(15) 910 | check(sessid, s) 911 | time.sleep(5) 912 | 913 | TG_BOT_TOKEN and TG_USER_ID and TG_API_HOST and telegram() 914 | RECEIVER_EMAIL and YD_EMAIL and YD_APP_PWD and email() 915 | SERVER_CHAN_SENDKEY and server_chan() 916 | 917 | print("*" * 30) 918 | -------------------------------------------------------------------------------- /serverless_function/README.md: -------------------------------------------------------------------------------- 1 | ## Variant for serverless function 2 | 3 | The entry is `main_handler`, and these two dependencies `requests` and `beautifulsoup4` need to be packaged (Compress into .zip file) together with the source code `main.py`, then deploy. 4 | 5 | -------------------------------------------------------------------------------- /serverless_function/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # 4 | # SPDX-FileCopyrightText: (c) 2020-2021 CokeMine & Its repository contributors 5 | # SPDX-FileCopyrightText: (c) 2021-2022 A beam of light 6 | # 7 | # SPDX-License-Identifier: GPL-3.0-or-later 8 | # 9 | 10 | """ 11 | euserv auto-renew script 12 | 13 | ChangeLog 14 | 15 | v2021.12.15 16 | - Implemented a simple localization system, log output localization 17 | - Reformat code via black 18 | 19 | v2021.11.26 20 | - Handle TrueCaptcha service exception 21 | - Adjust TrueCaptcha constraint parameters for high availability. 22 | Plus, the CAPTCHA of EUserv is currently case-insensitive, so the above adjustment works. 23 | 24 | v2021.11.06 25 | - Receive renew PIN(6-digits) using mailparser parsed data download url 26 | workflow: auto-forward your EUserv PIN email to your mailparser inbox 27 | -> parsing PIN via mailparser -> get PIN from mailparser 28 | - Update kc2_security_password_get_token request 29 | 30 | v2021.09.30 31 | - Captcha automatic recognition using TrueCaptcha API 32 | - Email notification 33 | - Add login failure retry mechanism 34 | - reformat log info 35 | 36 | """ 37 | 38 | import os 39 | import re 40 | import json 41 | import time 42 | import base64 43 | 44 | from email.mime.application import MIMEApplication 45 | from email.mime.multipart import MIMEMultipart 46 | from email.mime.text import MIMEText 47 | from smtplib import SMTP_SSL, SMTPDataError 48 | 49 | import requests 50 | from bs4 import BeautifulSoup 51 | 52 | # Please use one space to separate multiple accounts 53 | # euserv username or email 54 | USERNAME = os.environ["USERNAME"] 55 | # euserv password 56 | PASSWORD = os.environ["PASSWORD"] 57 | 58 | # default value is TrueCaptcha demo credential, 59 | # you can use your own credential via set environment variables: 60 | # TRUECAPTCHA_USERID and TRUECAPTCHA_APIKEY 61 | # demo: https://apitruecaptcha.org/demo 62 | # demo2: https://apitruecaptcha.org/demo2 63 | # demo apikey also has a limit of 100 times per day 64 | # { 65 | # 'error': '101.0 above free usage limit 100 per day and no balance', 66 | # 'requestId': '7690c065-70e0-4757-839b-5fd8381e65c7' 67 | # } 68 | TRUECAPTCHA_USERID = os.environ.get("TRUECAPTCHA_USERID", "arun56") 69 | TRUECAPTCHA_APIKEY = os.environ.get("TRUECAPTCHA_APIKEY", "wMjXmBIcHcdYqO2RrsVN") 70 | 71 | # Extract key data from your emails, automatically. https://mailparser.io 72 | # 30 Emails/Month, 10 inboxes and unlimited downloads for free. 73 | # Please use one space to separate multiple mailparser download link ids, 74 | # in order to correspond to the EUserv account/email. 75 | MAILPARSER_DOWNLOAD_URL_ID = os.environ["MAILPARSER_DOWNLOAD_URL_ID"] 76 | # mailparser parsed data download base url 77 | MAILPARSER_DOWNLOAD_BASE_URL = "https://files.mailparser.io/d/" 78 | 79 | # Telegram Bot Push https://core.telegram.org/bots/api#authorizing-your-bot 80 | # Obtained via @BotFather application, for example: 1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw 81 | TG_BOT_TOKEN = "" 82 | # User, group or channel ID, for example: 129xxx206 83 | TG_USER_ID = "" 84 | # Build your own API reverse proxy address for use when the network environment is inaccessible, 85 | # and keep the default if the network is normal. 86 | TG_API_HOST = "https://api.telegram.org" 87 | 88 | # Email notification via yandex service, you can modify yourself to use other email service notifications. 89 | RECEIVER_EMAIL = os.environ.get("RECEIVER_EMAIL", "") 90 | YD_EMAIL = os.environ.get("YD_EMAIL", "") 91 | # yandex mail using third party APP authorization code 92 | YD_APP_PWD = os.environ.get("YD_APP_PWD", "") 93 | 94 | # Server Chan(Server 酱, name in Chinese) https://sct.ftqq.com 95 | # Free quota: up to 5 messages per day, cards show only title, maximum 1000 API requests per day, up to 5 messages per minute. 96 | # Server Chan SENDKEY, no need to push can be ignored 97 | SERVER_CHAN_SENDKEY = os.environ.get("SERVER_CHAN_SENDKEY", "") 98 | 99 | # Magic internet access 100 | PROXIES = {"http": "http://127.0.0.1:10808", "https": "http://127.0.0.1:10808"} 101 | 102 | # Maximum number of login retry 103 | LOGIN_MAX_RETRY_COUNT = 5 104 | 105 | # Waiting time of receiving PIN, units are seconds. 106 | WAITING_TIME_OF_PIN = 15 107 | 108 | # Checking CAPTCHA API usage, options: True or False 109 | CHECK_CAPTCHA_SOLVER_USAGE = True 110 | 111 | user_agent = ( 112 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " 113 | "Chrome/96.0.4664.110 Safari/537.36" 114 | ) 115 | 116 | # Blank 117 | desp = "" 118 | 119 | # Simplified Chinese Translation 120 | chs_locale = { 121 | ":": ":", 122 | ",": ",", 123 | ".": "。", 124 | "!": "!", 125 | "...": "......", 126 | "~": "~", 127 | "Login retried the @@@ time": "登录重试第 @@@ 次", 128 | "You are using the demo apikey": "你正在使用演示版 apikey", 129 | "There is no guarantee that demo apikey will work in the future": "无法保证演示版 apikey 在将来也能使用", 130 | "You are using your own apikey": " 你正在使用自己的 apikey", 131 | "Service Exception": "服务异常", 132 | "Returned JSON": "返回的 JSON", 133 | "Failed to find parsed results": "找不到解析结果", 134 | "Performing CAPTCHA recognition": "进行验证码识别", 135 | "The recognized CAPTCHA is": "识别的验证码是", 136 | "current date": "当前日期", 137 | "api usage count": "api 使用次数", 138 | "CAPTCHA Verification passed": "CAPTCHA 验证通过", 139 | "CAPTCHA Verification failed": "CAPTCHA 验证失败", 140 | "PIN": "PIN", 141 | "ServerID": "服务器 ID", 142 | "Renew Failed": "续期失败", 143 | "ALL Work Done": "所有工作都已完成", 144 | "Enjoy": "使用愉快", 145 | "EUserv Renewal Logs": "EUserv 续期日志", 146 | "push failed": "推送失败", 147 | "push successfully": "推送成功", 148 | "Server Chan": "Server 酱", 149 | "Checking": "正在检查", 150 | "You have not added any accounts": "你没有添加任何账户", 151 | "The number of usernames and passwords do not match": "用户名和密码的数量不匹配", 152 | "The number of mailparser_dl_url_ids and usernames do not match": "mailparser 下载链接 id 和用户名的数量不匹配", 153 | "Renewing the @@@ account": "正在续期第 @@@ 个账号", 154 | "The @@@ account login failed": "第 @@@ 个账号登录失败", 155 | "please check the login information": "请检查登录信息", 156 | "renewals are being attempted": "正在尝试续期", 157 | "The @@@ account is detected": "检测到第 @@@ 个账号", 158 | "with @@@ VPS": "有 @@@ 台 VPS", 159 | "renew Error": "续期错误", 160 | "has been successfully renewed": "已成功续期", 161 | "does not need to be renewed": "不需要续期", 162 | } 163 | 164 | # Traditional Chinese Translation 165 | cht_locale = { 166 | ":": ":", 167 | ",": ",", 168 | ".": "。", 169 | "!": "!", 170 | "...": "......", 171 | "~": "~", 172 | "Login retried the @@@ time": "登錄重試第 @@@ 次", 173 | "You are using the demo apikey": "你正在使用演示版 apikey", 174 | "There is no guarantee that demo apikey will work in the future": "無法保證演示版 apikey 在將來也能使用", 175 | "You are using your own apikey": " 你正在使用你自己的 apikey", 176 | "Service Exception": "服務異常", 177 | "Returned JSON": "返回的 JSON", 178 | "Failed to find parsed results": "找不到解析結果", 179 | "Performing CAPTCHA recognition": "進行驗證碼識別", 180 | "The recognized CAPTCHA is": "識別的驗證碼是", 181 | "current date": "當前日期", 182 | "api usage count": "api 已使用次數", 183 | "CAPTCHA Verification passed": "CAPTCHA 驗證通過", 184 | "CAPTCHA Verification failed": "CAPTCHA 驗證失敗", 185 | "PIN": "PIN", 186 | "ServerID": "伺服器 ID", 187 | "Renew Failed": "續期失敗", 188 | "ALL Work Done": "所有工作都已完成", 189 | "Enjoy": "使用愉快", 190 | "EUserv Renewal Logs": "EUserv 續期日誌", 191 | "push failed": "推送失敗", 192 | "push successfully": "推送成功", 193 | "Server Chan": "Server 醬", 194 | "Checking": "正在檢查", 195 | "You have not added any accounts": "你沒有新增任何賬戶", 196 | "The number of usernames and passwords do not match": "使用者名稱和密碼的數量不匹配", 197 | "The number of mailparser_dl_url_ids and usernames do not match": "mailparser 下載連結 id 和使用者名稱的數量不匹配", 198 | "Renewing the @@@ account": "正在續期第 @@@ 個賬號", 199 | "The @@@ account login failed": "第 @@@ 個賬號登入失敗", 200 | "please check the login information": "請檢查登入資訊", 201 | "renewals are being attempted": "正在嘗試續期", 202 | "The @@@ account is detected": "檢測到第 @@@ 個賬號", 203 | "with @@@ VPS": "有 @@@ 臺 VPS", 204 | "renew Error": "續期錯誤", 205 | "has been successfully renewed": "已成功續期", 206 | "does not need to be renewed": "不需要續期", 207 | } 208 | 209 | # Localization 210 | log_lang_options = { 211 | "en": lambda x: x, 212 | "chs": lambda x: chs_locale.get(x, x), 213 | "cht": lambda x: cht_locale.get(x, x), 214 | } 215 | 216 | # Language Options: en/chs/cht, or leave it blank 217 | log_lang = "chs" 218 | 219 | ordinal = lambda n: "{}{}".format( 220 | n, 221 | "tsnrhtdd"[(n / 10 % 10 != 1) * (n % 10 < 4) * n % 10 :: 4], 222 | ) 223 | 224 | 225 | def log(info: str): 226 | print(info) 227 | global desp 228 | desp = desp + info + "\n\n" 229 | 230 | 231 | def login_retry(*args, **kwargs): 232 | def wrapper(func): 233 | def inner(username, password): 234 | ret, ret_session = func(username, password) 235 | max_retry = kwargs.get("max_retry") 236 | # default retry 3 times 237 | if not max_retry: 238 | max_retry = 3 239 | number = 0 240 | if ret == "-1": 241 | while number < max_retry: 242 | number += 1 243 | if number > 1: 244 | log( 245 | "[EUserv] {} {}".format( 246 | log_lang_options.get(log_lang, lambda x: x)( 247 | "Login retried the @@@ time" 248 | ).replace("@@@", ordinal(number)), 249 | log_lang_options.get(log_lang, lambda x: x)("."), 250 | ) 251 | ) 252 | sess_id, session = func(username, password) 253 | if sess_id != "-1": 254 | return sess_id, session 255 | else: 256 | if number == max_retry: 257 | return sess_id, session 258 | else: 259 | return ret, ret_session 260 | 261 | return inner 262 | 263 | return wrapper 264 | 265 | 266 | def captcha_solver(captcha_image_url: str, session: requests.session) -> dict: 267 | """ 268 | TrueCaptcha API doc: https://apitruecaptcha.org/api 269 | Free to use 100 requests per day. 270 | -- response:: 271 | { 272 | "result": "", ==> Or "result": 0 273 | "conf": 0.85, 274 | "usage": 0, 275 | "requestId": "ed0006e5-69f0-4617-b698-97dc054f9022", 276 | "version": "dev2" 277 | } 278 | """ 279 | response = session.get(captcha_image_url) 280 | encoded_string = base64.b64encode(response.content) 281 | url = "https://api.apitruecaptcha.org/one/gettext" 282 | 283 | # Since "case": "mixed", "mode": "human" 284 | # can sometimes cause internal errors in the truecaptcha server. 285 | # So a more relaxed constraint(lower/upper & default) is used here. 286 | # Plus, the CAPTCHA of EUserv is currently case-insensitive, so the below adjustment works. 287 | data = { 288 | "userid": TRUECAPTCHA_USERID, 289 | "apikey": TRUECAPTCHA_APIKEY, 290 | # case sensitivity of text (upper | lower| mixed) 291 | "case": "lower", 292 | # use human or AI (human | default) 293 | "mode": "default", 294 | "data": str(encoded_string)[2:-1], 295 | } 296 | r = requests.post(url=url, json=data) 297 | j = json.loads(r.text) 298 | return j 299 | 300 | 301 | def handle_captcha_solved_result(solved: dict) -> str: 302 | """Since CAPTCHA sometimes appears as a very simple binary arithmetic expression. 303 | But since recognition sometimes doesn't show the result of the calculation directly, 304 | that's what this function is for. 305 | """ 306 | if "result" in solved: 307 | solved_result = solved["result"] 308 | if isinstance(solved_result, str): 309 | if "RESULT IS" in solved_result: 310 | log( 311 | "[Captcha Solver] {}{}".format( 312 | log_lang_options.get(log_lang, lambda x: x)( 313 | "You are using the demo apikey" 314 | ), 315 | log_lang_options.get(log_lang, lambda x: x)("."), 316 | ) 317 | ) 318 | print( 319 | "{}{}".format( 320 | log_lang_options.get(log_lang, lambda x: x)( 321 | "There is no guarantee that demo apikey will work in the future" 322 | ), 323 | log_lang_options.get(log_lang, lambda x: x)("!"), 324 | ) 325 | ) 326 | # because using demo apikey 327 | text = re.findall(r"RESULT IS . (.*) .", solved_result)[0] 328 | else: 329 | # using your own apikey 330 | log( 331 | "[Captcha Solver] {}{}".format( 332 | log_lang_options.get(log_lang, lambda x: x)( 333 | "You are using your own apikey" 334 | ), 335 | log_lang_options.get(log_lang, lambda x: x)("."), 336 | ) 337 | ) 338 | text = solved_result 339 | operators = ["X", "x", "+", "-"] 340 | if any(x in text for x in operators): 341 | for operator in operators: 342 | operator_pos = text.find(operator) 343 | if operator == "x" or operator == "X": 344 | operator = "*" 345 | if operator_pos != -1: 346 | left_part = text[:operator_pos] 347 | right_part = text[operator_pos + 1 :] 348 | if left_part.isdigit() and right_part.isdigit(): 349 | return eval( 350 | "{left} {operator} {right}".format( 351 | left=left_part, operator=operator, right=right_part 352 | ) 353 | ) 354 | else: 355 | # Because these symbols("X", "x", "+", "-") do not appear at the same time, 356 | # it just contains an arithmetic symbol. 357 | return text 358 | else: 359 | return text 360 | else: 361 | print( 362 | "[Captcha Solver] {}{} {}".format( 363 | log_lang_options.get(log_lang, lambda x: x)("Returned JSON"), 364 | log_lang_options.get(log_lang, lambda x: x)(":"), 365 | solved, 366 | ) 367 | ) 368 | log( 369 | "[Captcha Solver] {}{}".format( 370 | log_lang_options.get(log_lang, lambda x: x)("Service Exception"), 371 | log_lang_options.get(log_lang, lambda x: x)("!"), 372 | ) 373 | ) 374 | raise ValueError("[Captcha Solver] Service Exception!") 375 | else: 376 | print( 377 | "[Captcha Solver] {}{} {}".format( 378 | log_lang_options.get(log_lang, lambda x: x)("Returned JSON"), 379 | log_lang_options.get(log_lang, lambda x: x)(":"), 380 | solved, 381 | ) 382 | ) 383 | log( 384 | "[Captcha Solver] {}{}".format( 385 | log_lang_options.get(log_lang, lambda x: x)( 386 | "Failed to find parsed results" 387 | ), 388 | log_lang_options.get(log_lang, lambda x: x)("!"), 389 | ) 390 | ) 391 | raise KeyError("[Captcha Solver] Failed to find parsed results!") 392 | 393 | 394 | def get_captcha_solver_usage() -> dict: 395 | url = "https://api.apitruecaptcha.org/one/getusage" 396 | 397 | params = { 398 | "username": TRUECAPTCHA_USERID, 399 | "apikey": TRUECAPTCHA_APIKEY, 400 | } 401 | r = requests.get(url=url, params=params) 402 | j = json.loads(r.text) 403 | return j 404 | 405 | 406 | def get_pin_from_mailparser(url_id: str) -> str: 407 | """ 408 | response format: 409 | [ 410 | { 411 | "id": "83b95f50f6202fb03950afbe00975eab", 412 | "received_at": "2021-11-06 02:30:07", ==> up to mailparser account timezone setting, here is UTC 0000. 413 | "processed_at": "2021-11-06 02:30:07", 414 | "pin": "123456" 415 | } 416 | ] 417 | """ 418 | response = requests.get( 419 | f"{MAILPARSER_DOWNLOAD_BASE_URL}{url_id}", 420 | # Mailparser parsed data download using Basic Authentication. 421 | # auth=("", "") 422 | ) 423 | pin = response.json()[0]["pin"] 424 | return pin 425 | 426 | 427 | @login_retry(max_retry=LOGIN_MAX_RETRY_COUNT) 428 | def login(username: str, password: str) -> (str, requests.session): 429 | headers = {"user-agent": user_agent, "origin": "https://www.euserv.com"} 430 | url = "https://support.euserv.com/index.iphp" 431 | captcha_image_url = "https://support.euserv.com/securimage_show.php" 432 | session = requests.Session() 433 | 434 | sess = session.get(url, headers=headers) 435 | sess_id = re.findall("PHPSESSID=(\\w{10,100});", str(sess.headers))[0] 436 | # visit png 437 | logo_png_url = "https://support.euserv.com/pic/logo_small.png" 438 | session.get(logo_png_url, headers=headers) 439 | 440 | login_data = { 441 | "email": username, 442 | "password": password, 443 | "form_selected_language": "en", 444 | "Submit": "Login", 445 | "subaction": "login", 446 | "sess_id": sess_id, 447 | } 448 | f = session.post(url, headers=headers, data=login_data) 449 | f.raise_for_status() 450 | 451 | if ( 452 | f.text.find("Hello") == -1 453 | and f.text.find("Confirm or change your customer data here") == -1 454 | ): 455 | if ( 456 | f.text.find( 457 | "To finish the login process please solve the following captcha." 458 | ) 459 | == -1 460 | ): 461 | return "-1", session 462 | else: 463 | log( 464 | "[Captcha Solver] {}{}".format( 465 | log_lang_options.get(log_lang, lambda x: x)( 466 | "Performing CAPTCHA recognition" 467 | ), 468 | log_lang_options.get(log_lang, lambda x: x)("..."), 469 | ) 470 | ) 471 | solved_result = captcha_solver(captcha_image_url, session) 472 | try: 473 | captcha_code = handle_captcha_solved_result(solved_result) 474 | log( 475 | "[Captcha Solver] {}{} {}".format( 476 | log_lang_options.get(log_lang, lambda x: x)( 477 | "The recognized CAPTCHA is" 478 | ), 479 | log_lang_options.get(log_lang, lambda x: x)(":"), 480 | captcha_code, 481 | ) 482 | ) 483 | 484 | if CHECK_CAPTCHA_SOLVER_USAGE: 485 | usage = get_captcha_solver_usage() 486 | log( 487 | "[Captcha Solver] {} {} {}{} {}".format( 488 | log_lang_options.get(log_lang, lambda x: x)("current date"), 489 | usage[0]["date"], 490 | log_lang_options.get(log_lang, lambda x: x)( 491 | "api usage count" 492 | ), 493 | log_lang_options.get(log_lang, lambda x: x)(":"), 494 | usage[0]["count"], 495 | ) 496 | ) 497 | 498 | f2 = session.post( 499 | url, 500 | headers=headers, 501 | data={ 502 | "subaction": "login", 503 | "sess_id": sess_id, 504 | "captcha_code": captcha_code, 505 | }, 506 | ) 507 | if ( 508 | f2.text.find( 509 | "To finish the login process please solve the following captcha." 510 | ) 511 | == -1 512 | ): 513 | log( 514 | "[Captcha Solver] {}".format( 515 | log_lang_options.get(log_lang, lambda x: x)( 516 | "CAPTCHA Verification passed" 517 | ) 518 | ) 519 | ) 520 | return sess_id, session 521 | else: 522 | log( 523 | "[Captcha Solver] {}".format( 524 | log_lang_options.get(log_lang, lambda x: x)( 525 | "CAPTCHA Verification failed" 526 | ) 527 | ) 528 | ) 529 | return "-1", session 530 | except (KeyError, ValueError): 531 | return "-1", session 532 | else: 533 | return sess_id, session 534 | 535 | 536 | def get_servers(sess_id: str, session: requests.session) -> {}: 537 | d = {} 538 | url = "https://support.euserv.com/index.iphp?sess_id=" + sess_id 539 | headers = {"user-agent": user_agent, "origin": "https://www.euserv.com"} 540 | f = session.get(url=url, headers=headers) 541 | f.raise_for_status() 542 | soup = BeautifulSoup(f.text, "html.parser") 543 | for tr in soup.select( 544 | "#kc2_order_customer_orders_tab_content_1 .kc2_order_table.kc2_content_table tr" 545 | ): 546 | server_id = tr.select(".td-z1-sp1-kc") 547 | if not len(server_id) == 1: 548 | continue 549 | flag = ( 550 | True 551 | if tr.select(".td-z1-sp2-kc .kc2_order_action_container")[0] 552 | .get_text() 553 | .find("Contract extension possible from") 554 | == -1 555 | else False 556 | ) 557 | d[server_id[0].get_text()] = flag 558 | return d 559 | 560 | 561 | def renew( 562 | sess_id: str, 563 | session: requests.session, 564 | password: str, 565 | order_id: str, 566 | mailparser_dl_url_id: str, 567 | ) -> bool: 568 | url = "https://support.euserv.com/index.iphp" 569 | headers = { 570 | "user-agent": user_agent, 571 | "Host": "support.euserv.com", 572 | "origin": "https://support.euserv.com", 573 | "Referer": "https://support.euserv.com/index.iphp", 574 | } 575 | data = { 576 | "Submit": "Extend contract", 577 | "sess_id": sess_id, 578 | "ord_no": order_id, 579 | "subaction": "choose_order", 580 | "choose_order_subaction": "show_contract_details", 581 | } 582 | session.post(url, headers=headers, data=data) 583 | 584 | # pop up 'Security Check' window, it will trigger 'send PIN' automatically. 585 | session.post( 586 | url, 587 | headers=headers, 588 | data={ 589 | "sess_id": sess_id, 590 | "subaction": "show_kc2_security_password_dialog", 591 | "prefix": "kc2_customer_contract_details_extend_contract_", 592 | "type": "1", 593 | }, 594 | ) 595 | 596 | # # trigger 'Send new PIN to your Email-Address' (optional), 597 | # new_pin = session.post(url, headers=headers, data={ 598 | # "sess_id": sess_id, 599 | # "subaction": "kc2_security_password_send_pin", 600 | # "ident": f"kc2_customer_contract_details_extend_contract_{order_id}" 601 | # }) 602 | # if not json.loads(new_pin.text)["rc"] == "100": 603 | # print("new PIN Not Sended") 604 | # return False 605 | 606 | # sleep WAITING_TIME_OF_PIN seconds waiting for mailparser email parsed PIN 607 | time.sleep(WAITING_TIME_OF_PIN) 608 | pin = get_pin_from_mailparser(mailparser_dl_url_id) 609 | log( 610 | "[MailParser] {}{} {}".format( 611 | log_lang_options.get(log_lang, lambda x: x)("PIN"), 612 | log_lang_options.get(log_lang, lambda x: x)(":"), 613 | pin, 614 | ) 615 | ) 616 | 617 | # using PIN instead of password to get token 618 | data = { 619 | "auth": pin, 620 | "sess_id": sess_id, 621 | "subaction": "kc2_security_password_get_token", 622 | "prefix": "kc2_customer_contract_details_extend_contract_", 623 | "type": 1, 624 | "ident": f"kc2_customer_contract_details_extend_contract_{order_id}", 625 | } 626 | f = session.post(url, headers=headers, data=data) 627 | f.raise_for_status() 628 | if not json.loads(f.text)["rs"] == "success": 629 | return False 630 | token = json.loads(f.text)["token"]["value"] 631 | data = { 632 | "sess_id": sess_id, 633 | "ord_id": order_id, 634 | "subaction": "kc2_customer_contract_details_extend_contract_term", 635 | "token": token, 636 | } 637 | session.post(url, headers=headers, data=data) 638 | time.sleep(5) 639 | return True 640 | 641 | 642 | def check(sess_id: str, session: requests.session): 643 | print( 644 | "{}{}".format( 645 | log_lang_options.get(log_lang, lambda x: x)("Checking"), 646 | log_lang_options.get(log_lang, lambda x: x)("..."), 647 | ) 648 | ) 649 | d = get_servers(sess_id, session) 650 | flag = True 651 | for key, val in d.items(): 652 | if val: 653 | flag = False 654 | log( 655 | "[EUserv] {}{} {} {}{}".format( 656 | log_lang_options.get(log_lang, lambda x: x)("ServerID"), 657 | log_lang_options.get(log_lang, lambda x: x)(":"), 658 | key, 659 | log_lang_options.get(log_lang, lambda x: x)("Renew Failed"), 660 | log_lang_options.get(log_lang, lambda x: x)("!"), 661 | ) 662 | ) 663 | 664 | if flag: 665 | log( 666 | "[EUserv] {}{} {}{}".format( 667 | log_lang_options.get(log_lang, lambda x: x)("ALL Work Done"), 668 | log_lang_options.get(log_lang, lambda x: x)("!"), 669 | log_lang_options.get(log_lang, lambda x: x)("Enjoy"), 670 | log_lang_options.get(log_lang, lambda x: x)("~"), 671 | ) 672 | ) 673 | 674 | 675 | # Telegram Bot Push https://core.telegram.org/bots/api#authorizing-your-bot 676 | def telegram(): 677 | data = ( 678 | ("chat_id", TG_USER_ID), 679 | ( 680 | "text", 681 | "{}\n\n".format( 682 | log_lang_options.get(log_lang, lambda x: x)("EUserv Renewal Logs") 683 | ) 684 | + desp, 685 | ), 686 | ) 687 | response = requests.post( 688 | TG_API_HOST + "/bot" + TG_BOT_TOKEN + "/sendMessage", data=data 689 | ) 690 | if response.status_code != 200: 691 | print( 692 | "Telegram Bot {}".format( 693 | log_lang_options.get(log_lang, lambda x: x)("push failed") 694 | ) 695 | ) 696 | else: 697 | print( 698 | "Telegram Bot {}".format( 699 | log_lang_options.get(log_lang, lambda x: x)("push successfully") 700 | ) 701 | ) 702 | 703 | 704 | # Yandex mail notification 705 | def send_mail_by_yandex( 706 | to_email, from_email, subject, text, files, sender_email, sender_password 707 | ): 708 | msg = MIMEMultipart() 709 | msg["Subject"] = subject 710 | msg["From"] = from_email 711 | msg["To"] = to_email 712 | msg.attach(MIMEText(text, _charset="utf-8")) 713 | if files is not None: 714 | for file in files: 715 | file_name, file_content = file 716 | # print(file_name) 717 | part = MIMEApplication(file_content) 718 | part.add_header( 719 | "Content-Disposition", "attachment", filename=("gb18030", "", file_name) 720 | ) 721 | msg.attach(part) 722 | s = SMTP_SSL("smtp.yandex.ru", 465) 723 | s.login(sender_email, sender_password) 724 | try: 725 | s.sendmail(msg["From"], msg["To"], msg.as_string()) 726 | except SMTPDataError as e: 727 | raise e 728 | finally: 729 | s.close() 730 | 731 | 732 | # eMail push 733 | def email(): 734 | msg = ( 735 | "{}\n\n".format( 736 | log_lang_options.get(log_lang, lambda x: x)("EUserv Renewal Logs") 737 | ) 738 | + desp 739 | ) 740 | try: 741 | send_mail_by_yandex( 742 | RECEIVER_EMAIL, 743 | YD_EMAIL, 744 | log_lang_options.get(log_lang, lambda x: x)("EUserv Renewal Logs"), 745 | msg, 746 | None, 747 | YD_EMAIL, 748 | YD_APP_PWD, 749 | ) 750 | print( 751 | "eMail {}".format( 752 | log_lang_options.get(log_lang, lambda x: x)("push successfully") 753 | ) 754 | ) 755 | except requests.exceptions.RequestException as e: 756 | print(str(e)) 757 | print( 758 | "eMail {}".format( 759 | log_lang_options.get(log_lang, lambda x: x)("push failed") 760 | ) 761 | ) 762 | except SMTPDataError as e1: 763 | print(str(e1)) 764 | print( 765 | "eMail {}".format( 766 | log_lang_options.get(log_lang, lambda x: x)("push failed") 767 | ) 768 | ) 769 | 770 | 771 | # Server Chan https://sct.ftqq.com 772 | def server_chan(): 773 | data = { 774 | "title": log_lang_options.get(log_lang, lambda x: x)("EUserv Renewal Logs"), 775 | "desp": desp, 776 | } 777 | response = requests.post( 778 | f"https://sctapi.ftqq.com/{SERVER_CHAN_SENDKEY}.send", data=data 779 | ) 780 | if response.status_code != 200: 781 | print( 782 | "{} {}".format( 783 | log_lang_options.get(log_lang, lambda x: x)("Server Chan"), 784 | log_lang_options.get(log_lang, lambda x: x)("push failed"), 785 | ) 786 | ) 787 | else: 788 | print( 789 | "{} {}".format( 790 | log_lang_options.get(log_lang, lambda x: x)("Server Chan"), 791 | log_lang_options.get(log_lang, lambda x: x)("push successfully"), 792 | ) 793 | ) 794 | 795 | 796 | def main_handler(event, context): 797 | if not USERNAME or not PASSWORD: 798 | log( 799 | "[EUserv] {}{}".format( 800 | log_lang_options.get(log_lang, lambda x: x)( 801 | "You have not added any accounts" 802 | ), 803 | log_lang_options.get(log_lang, lambda x: x)("."), 804 | ) 805 | ) 806 | exit(1) 807 | user_list = USERNAME.strip().split() 808 | passwd_list = PASSWORD.strip().split() 809 | mailparser_dl_url_id_list = MAILPARSER_DOWNLOAD_URL_ID.strip().split() 810 | if len(user_list) != len(passwd_list): 811 | log( 812 | "[EUserv] {}{}".format( 813 | log_lang_options.get(log_lang, lambda x: x)( 814 | "The number of usernames and passwords do not match" 815 | ), 816 | log_lang_options.get(log_lang, lambda x: x)("!"), 817 | ) 818 | ) 819 | exit(1) 820 | if len(mailparser_dl_url_id_list) != len(user_list): 821 | log( 822 | "[Mailparser] {}{}".format( 823 | log_lang_options.get(log_lang, lambda x: x)( 824 | "The number of mailparser_dl_url_ids and usernames do not match" 825 | ), 826 | log_lang_options.get(log_lang, lambda x: x)("!"), 827 | ) 828 | ) 829 | exit(1) 830 | for i in range(len(user_list)): 831 | print("*" * 30) 832 | log( 833 | "[EUserv] {}{}".format( 834 | log_lang_options.get(log_lang, lambda x: x)( 835 | "Renewing the @@@ account" 836 | ).replace("@@@", ordinal(i + 1)), 837 | log_lang_options.get(log_lang, lambda x: x)("..."), 838 | ) 839 | ) 840 | sessid, s = login(user_list[i], passwd_list[i]) 841 | if sessid == "-1": 842 | log( 843 | "[EUserv] {}{} {}{}".format( 844 | log_lang_options.get(log_lang, lambda x: x)( 845 | "The @@@ account login failed" 846 | ).replace("@@@", ordinal(i + 1)), 847 | log_lang_options.get(log_lang, lambda x: x)(","), 848 | log_lang_options.get(log_lang, lambda x: x)( 849 | "please check the login information" 850 | ), 851 | log_lang_options.get(log_lang, lambda x: x)("."), 852 | ) 853 | ) 854 | continue 855 | SERVERS = get_servers(sessid, s) 856 | log( 857 | "[EUserv] {} {}{} {}{}".format( 858 | log_lang_options.get(log_lang, lambda x: x)( 859 | "The @@@ account is detected" 860 | ).replace("@@@", ordinal(i + 1)), 861 | log_lang_options.get(log_lang, lambda x: x)("with @@@ VPS").replace( 862 | "@@@", str(len(SERVERS)) 863 | ), 864 | log_lang_options.get(log_lang, lambda x: x)(","), 865 | log_lang_options.get(log_lang, lambda x: x)( 866 | "renewals are being attempted" 867 | ), 868 | log_lang_options.get(log_lang, lambda x: x)("..."), 869 | ) 870 | ) 871 | for k, v in SERVERS.items(): 872 | if v: 873 | if not renew( 874 | sessid, s, passwd_list[i], k, mailparser_dl_url_id_list[i] 875 | ): 876 | log( 877 | "[EUserv] {}{} {} {}{}".format( 878 | log_lang_options.get(log_lang, lambda x: x)("ServerID"), 879 | log_lang_options.get(log_lang, lambda x: x)(":"), 880 | k, 881 | log_lang_options.get(log_lang, lambda x: x)("renew Error"), 882 | log_lang_options.get(log_lang, lambda x: x)("!"), 883 | ) 884 | ) 885 | else: 886 | log( 887 | "[EUserv] {}{} {} {}{}".format( 888 | log_lang_options.get(log_lang, lambda x: x)("ServerID"), 889 | log_lang_options.get(log_lang, lambda x: x)(":"), 890 | k, 891 | log_lang_options.get(log_lang, lambda x: x)( 892 | "has been successfully renewed" 893 | ), 894 | log_lang_options.get(log_lang, lambda x: x)("!"), 895 | ) 896 | ) 897 | else: 898 | log( 899 | "[EUserv] {}{} {} {}{}".format( 900 | log_lang_options.get(log_lang, lambda x: x)("ServerID"), 901 | log_lang_options.get(log_lang, lambda x: x)(":"), 902 | k, 903 | log_lang_options.get(log_lang, lambda x: x)( 904 | "does not need to be renewed" 905 | ), 906 | log_lang_options.get(log_lang, lambda x: x)("."), 907 | ) 908 | ) 909 | time.sleep(15) 910 | check(sessid, s) 911 | time.sleep(5) 912 | 913 | TG_BOT_TOKEN and TG_USER_ID and TG_API_HOST and telegram() 914 | RECEIVER_EMAIL and YD_EMAIL and YD_APP_PWD and email() 915 | SERVER_CHAN_SENDKEY and server_chan() 916 | 917 | print("*" * 30) 918 | 919 | 920 | # only for debug 921 | # if __name__ == '__main__': 922 | # main_handler(None, None) 923 | --------------------------------------------------------------------------------