tempmail_sdk 1.3.3 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/tempmail_sdk/providers/altmails.rb +99 -0
- data/lib/tempmail_sdk/providers/apihz.rb +138 -0
- data/lib/tempmail_sdk/providers/awamail.rb +95 -0
- data/lib/tempmail_sdk/providers/best_temp_mail.rb +77 -0
- data/lib/tempmail_sdk/providers/chatgpt_org_uk.rb +129 -0
- data/lib/tempmail_sdk/providers/disposablemail.rb +125 -0
- data/lib/tempmail_sdk/providers/disposablemail_app.rb +66 -0
- data/lib/tempmail_sdk/providers/email10min.rb +152 -0
- data/lib/tempmail_sdk/providers/emailnator.rb +132 -0
- data/lib/tempmail_sdk/providers/emailtemp_org.rb +131 -0
- data/lib/tempmail_sdk/providers/expressinboxhub.rb +118 -0
- data/lib/tempmail_sdk/providers/fakemail.rb +143 -0
- data/lib/tempmail_sdk/providers/haribu.rb +29 -3
- data/lib/tempmail_sdk/providers/linshiyouxiang_net.rb +85 -0
- data/lib/tempmail_sdk/providers/mail_sunls.rb +42 -1
- data/lib/tempmail_sdk/providers/mail_td.rb +161 -0
- data/lib/tempmail_sdk/providers/mailcat_ai.rb +52 -0
- data/lib/tempmail_sdk/providers/maildrop.rb +79 -4
- data/lib/tempmail_sdk/providers/mailgolem.rb +96 -0
- data/lib/tempmail_sdk/providers/mailinator.rb +9 -2
- data/lib/tempmail_sdk/providers/mailtemp_cc.rb +91 -0
- data/lib/tempmail_sdk/providers/minuteinbox.rb +174 -0
- data/lib/tempmail_sdk/providers/moakt.rb +29 -3
- data/lib/tempmail_sdk/providers/mohmal.rb +208 -0
- data/lib/tempmail_sdk/providers/mytempmail_cc.rb +76 -0
- data/lib/tempmail_sdk/providers/openinbox.rb +102 -0
- data/lib/tempmail_sdk/providers/smail_pw.rb +156 -0
- data/lib/tempmail_sdk/providers/socketio_mail.rb +25 -0
- data/lib/tempmail_sdk/providers/tempgbox.rb +118 -0
- data/lib/tempmail_sdk/providers/tempmail_cn.rb +111 -0
- data/lib/tempmail_sdk/providers/tenminutemail_net.rb +183 -0
- data/lib/tempmail_sdk/providers/twentyfourmail_chacuo.rb +83 -0
- data/lib/tempmail_sdk/providers/vip_215.rb +171 -0
- data/lib/tempmail_sdk/registry.rb +163 -1
- data/lib/tempmail_sdk/version.rb +1 -1
- metadata +30 -2
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# mailgolem.com 渠道实现
|
|
8
|
+
#
|
|
9
|
+
# 流程:GET / 获取 session cookie + CSRF token → GET /random-email-address 创建邮箱
|
|
10
|
+
# GET / 重新获取 session + CSRF → POST /fetch-emails/{email} 获取邮件列表
|
|
11
|
+
# token 存储 CSRF token 值(收信时重新建立 session)
|
|
12
|
+
module Mailgolem
|
|
13
|
+
CHANNEL = "mailgolem"
|
|
14
|
+
BASE_URL = "https://mailgolem.com"
|
|
15
|
+
|
|
16
|
+
BROWSER_HEADERS = {
|
|
17
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
18
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
|
|
19
|
+
"Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif," \
|
|
20
|
+
"image/webp,image/apng,*/*;q=0.8",
|
|
21
|
+
"Accept-Language" => "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7"
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
CSRF_RE = /<input[^>]+name="_token"[^>]+id="token"[^>]+value="([^"]*)"/im
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# 从首页 HTML 提取 CSRF token
|
|
29
|
+
# @param html [String]
|
|
30
|
+
# @return [String]
|
|
31
|
+
def extract_csrf(html)
|
|
32
|
+
m = html.match(CSRF_RE)
|
|
33
|
+
m ? m[1].strip : ""
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# 访问首页建立 session 并提取 CSRF token
|
|
37
|
+
# @return [String] csrf token
|
|
38
|
+
def fetch_csrf
|
|
39
|
+
resp = Http.get(BASE_URL + "/", headers: BROWSER_HEADERS, timeout: 15)
|
|
40
|
+
resp.raise_for_status
|
|
41
|
+
csrf = extract_csrf(resp.body)
|
|
42
|
+
raise "mailgolem: 未能从首页提取 CSRF token" if csrf.empty?
|
|
43
|
+
|
|
44
|
+
csrf
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# 创建 mailgolem.com 临时邮箱
|
|
48
|
+
# @return [EmailInfo]
|
|
49
|
+
def generate_email
|
|
50
|
+
csrf = fetch_csrf
|
|
51
|
+
resp = Http.get(
|
|
52
|
+
BASE_URL + "/random-email-address",
|
|
53
|
+
headers: BROWSER_HEADERS.merge("Referer" => BASE_URL + "/"),
|
|
54
|
+
timeout: 15
|
|
55
|
+
)
|
|
56
|
+
resp.raise_for_status
|
|
57
|
+
email = resp.body.to_s.strip
|
|
58
|
+
raise "mailgolem: 获取到的邮箱地址无效" if email.empty? || !email.include?("@")
|
|
59
|
+
|
|
60
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: csrf)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 获取 mailgolem.com 邮件列表
|
|
64
|
+
# @param email [String]
|
|
65
|
+
# @param token [String] 旧 CSRF token(收信时重新建立 session)
|
|
66
|
+
# @return [Array<Email>]
|
|
67
|
+
def get_emails(email, token)
|
|
68
|
+
_ = token
|
|
69
|
+
csrf = fetch_csrf
|
|
70
|
+
form = URI.encode_www_form("_token" => csrf)
|
|
71
|
+
fetch_url = "#{BASE_URL}/fetch-emails/#{URI.encode_uri_component(email)}"
|
|
72
|
+
resp = Http.post(
|
|
73
|
+
fetch_url,
|
|
74
|
+
headers: BROWSER_HEADERS.merge(
|
|
75
|
+
"Content-Type" => "application/x-www-form-urlencoded",
|
|
76
|
+
"X-Requested-With" => "XMLHttpRequest",
|
|
77
|
+
"Accept" => "application/json, text/plain, */*",
|
|
78
|
+
"Referer" => BASE_URL + "/",
|
|
79
|
+
"Origin" => BASE_URL
|
|
80
|
+
),
|
|
81
|
+
body: form,
|
|
82
|
+
timeout: 15
|
|
83
|
+
)
|
|
84
|
+
resp.raise_for_status
|
|
85
|
+
items = resp.json
|
|
86
|
+
return [] unless items.is_a?(Array)
|
|
87
|
+
|
|
88
|
+
items.filter_map do |item|
|
|
89
|
+
next unless item.is_a?(Hash)
|
|
90
|
+
|
|
91
|
+
Normalize.normalize_email(item.merge("to" => email), email)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -136,13 +136,20 @@ module TempmailSdk
|
|
|
136
136
|
|
|
137
137
|
# 将邮件摘要与正文/附件展平为 normalize 可识别的字段
|
|
138
138
|
def flatten_message(summary, recipient_email, text_payload, html_payload, att_payload)
|
|
139
|
+
# 优先读 "text" 键(/text 端点实际返回键),回退兜底 "text/plain"(防御性编程)
|
|
140
|
+
text_content = text_from_payload(text_payload, "text")
|
|
141
|
+
text_content = text_from_payload(text_payload, "text/plain") if text_content.empty?
|
|
142
|
+
# 优先读 "html" 键,回退兜底 "text/html"
|
|
143
|
+
html_content = text_from_payload(html_payload, "html")
|
|
144
|
+
html_content = text_from_payload(html_payload, "text/html") if html_content.empty?
|
|
145
|
+
|
|
139
146
|
{
|
|
140
147
|
"id" => (summary["id"] || summary["messageId"] || "").to_s,
|
|
141
148
|
"from" => first_non_empty(summary["from"], summary["origfrom"]),
|
|
142
149
|
"to" => summary["to"] || recipient_email,
|
|
143
150
|
"subject" => summary["subject"] || "",
|
|
144
|
-
"text" =>
|
|
145
|
-
"html" =>
|
|
151
|
+
"text" => text_content,
|
|
152
|
+
"html" => html_content,
|
|
146
153
|
"date" => to_iso_time(summary["time"] || summary["date"]),
|
|
147
154
|
"seen" => false,
|
|
148
155
|
"attachments" => build_attachments(att_payload)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# mailtemp.cc 渠道实现(PHP POST form-urlencoded API)
|
|
8
|
+
#
|
|
9
|
+
# 流程:POST api.php body: action=inbox 创建邮箱(返回 JSON 字符串用户名)
|
|
10
|
+
# POST api.php body: action=fetch&inbox={username}&last_id=0 获取邮件列表
|
|
11
|
+
# POST api.php body: action=view&id={id}&inbox={username} 获取邮件详情
|
|
12
|
+
# token 存储 username(@前面的部分),域名固定 neocea.com
|
|
13
|
+
module MailtempCc
|
|
14
|
+
CHANNEL = "mailtemp-cc"
|
|
15
|
+
API_URL = "https://mailtemp.cc/api.php"
|
|
16
|
+
|
|
17
|
+
HEADERS = {
|
|
18
|
+
"Content-Type" => "application/x-www-form-urlencoded",
|
|
19
|
+
"Accept" => "application/json, text/plain, */*",
|
|
20
|
+
"Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
|
|
21
|
+
"Referer" => "https://mailtemp.cc/",
|
|
22
|
+
"Origin" => "https://mailtemp.cc",
|
|
23
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
24
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# 创建 mailtemp.cc 临时邮箱
|
|
30
|
+
# @return [EmailInfo]
|
|
31
|
+
def generate_email
|
|
32
|
+
resp = Http.post(API_URL, headers: HEADERS, body: "action=inbox", timeout: 15)
|
|
33
|
+
resp.raise_for_status
|
|
34
|
+
# 返回值为 JSON 字符串格式(带引号),如 "vindictiverate"
|
|
35
|
+
username = resp.json
|
|
36
|
+
username = username.to_s.strip
|
|
37
|
+
raise "mailtemp-cc: 返回的用户名为空" if username.empty?
|
|
38
|
+
|
|
39
|
+
EmailInfo.new(channel: CHANNEL, email: "#{username}@neocea.com", token: username)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# 获取 mailtemp.cc 邮件列表
|
|
43
|
+
# @param email [String]
|
|
44
|
+
# @param token [String] username
|
|
45
|
+
# @return [Array<Email>]
|
|
46
|
+
def get_emails(email, token)
|
|
47
|
+
token = token.to_s.strip
|
|
48
|
+
raise "mailtemp-cc: token 为空" if token.empty?
|
|
49
|
+
|
|
50
|
+
form = URI.encode_www_form("action" => "fetch", "inbox" => token, "last_id" => "0")
|
|
51
|
+
resp = Http.post(API_URL, headers: HEADERS, body: form, timeout: 15)
|
|
52
|
+
resp.raise_for_status
|
|
53
|
+
items = resp.json
|
|
54
|
+
return [] unless items.is_a?(Array)
|
|
55
|
+
return [] if items.empty?
|
|
56
|
+
|
|
57
|
+
items.filter_map do |item|
|
|
58
|
+
next unless item.is_a?(Hash)
|
|
59
|
+
|
|
60
|
+
mail_id = item["id"].to_s
|
|
61
|
+
unless mail_id.empty?
|
|
62
|
+
detail = view_email(token, mail_id)
|
|
63
|
+
if detail.is_a?(Hash)
|
|
64
|
+
item["html"] = detail["body_html"] if detail["body_html"]
|
|
65
|
+
item["body_html"] = detail["body_html"] if detail["body_html"]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
item["from"] ||= item["sender_email"] || item["sender"]
|
|
70
|
+
item["date"] ||= item["received_at"]
|
|
71
|
+
item["to"] = email
|
|
72
|
+
Normalize.normalize_email(item, email)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# 获取单封邮件详情
|
|
77
|
+
# @param inbox [String] username
|
|
78
|
+
# @param mail_id [String]
|
|
79
|
+
# @return [Hash, nil]
|
|
80
|
+
def view_email(inbox, mail_id)
|
|
81
|
+
form = URI.encode_www_form("action" => "view", "id" => mail_id, "inbox" => inbox)
|
|
82
|
+
resp = Http.post(API_URL, headers: HEADERS, body: form, timeout: 15)
|
|
83
|
+
return nil unless resp.ok?
|
|
84
|
+
|
|
85
|
+
resp.json
|
|
86
|
+
rescue StandardError
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module TempmailSdk
|
|
7
|
+
module Providers
|
|
8
|
+
# minuteinbox.com 渠道实现(PHP session + CSRF token)
|
|
9
|
+
#
|
|
10
|
+
# 流程:GET / 获取 PHPSESSID cookie 和 CSRF token(const CSRF="xxx")
|
|
11
|
+
# GET /index/index?csrf_token={csrf} 创建邮箱,返回 {"email":"user@minafter.com"}
|
|
12
|
+
# GET /index/refresh 获取邮件列表
|
|
13
|
+
# POST /index/email body: id=X 获取邮件详情
|
|
14
|
+
# token 存储 JSON {"phpsessid":"...","csrf":"..."}
|
|
15
|
+
module Minuteinbox
|
|
16
|
+
CHANNEL = "minuteinbox"
|
|
17
|
+
BASE_URL = "https://www.minuteinbox.com"
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# 从 HTML 中提取 CSRF token(格式: const CSRF="xxx")
|
|
22
|
+
# @param html [String]
|
|
23
|
+
# @return [String]
|
|
24
|
+
def extract_csrf(html)
|
|
25
|
+
marker = 'CSRF="'
|
|
26
|
+
idx = html.index(marker)
|
|
27
|
+
return "" unless idx
|
|
28
|
+
|
|
29
|
+
sub = html[(idx + marker.length)..]
|
|
30
|
+
end_idx = sub.index('"')
|
|
31
|
+
end_idx ? sub[0...end_idx] : ""
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# 将 session 序列化为 JSON token
|
|
35
|
+
# @param phpsessid [String]
|
|
36
|
+
# @param csrf [String]
|
|
37
|
+
# @return [String]
|
|
38
|
+
def encode_session(phpsessid, csrf)
|
|
39
|
+
JSON.generate("phpsessid" => phpsessid, "csrf" => csrf)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# 从 JSON token 反序列化 session
|
|
43
|
+
# @param token [String]
|
|
44
|
+
# @return [Array<String>] [phpsessid, csrf]
|
|
45
|
+
def decode_session(token)
|
|
46
|
+
data = JSON.parse(token)
|
|
47
|
+
phpsessid = data["phpsessid"].to_s.strip
|
|
48
|
+
csrf = data["csrf"].to_s.strip
|
|
49
|
+
raise "minuteinbox: session token 缺少必要字段" if phpsessid.empty? || csrf.empty?
|
|
50
|
+
|
|
51
|
+
[phpsessid, csrf]
|
|
52
|
+
rescue JSON::ParserError
|
|
53
|
+
raise "minuteinbox: 无效的 session token"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# 创建 minuteinbox.com 临时邮箱
|
|
57
|
+
# @return [EmailInfo]
|
|
58
|
+
def generate_email
|
|
59
|
+
hdrs = {
|
|
60
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
61
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
|
|
62
|
+
"Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
|
63
|
+
}
|
|
64
|
+
r1 = Http.get(BASE_URL + "/", headers: hdrs, timeout: 15)
|
|
65
|
+
r1.raise_for_status
|
|
66
|
+
|
|
67
|
+
csrf = extract_csrf(r1.body)
|
|
68
|
+
raise "minuteinbox: 未能从首页提取 CSRF token" if csrf.empty?
|
|
69
|
+
|
|
70
|
+
phpsessid = ""
|
|
71
|
+
r1.set_cookies.each do |line|
|
|
72
|
+
pair = line.split(";", 2).first.to_s.strip
|
|
73
|
+
next unless pair.start_with?("PHPSESSID=")
|
|
74
|
+
|
|
75
|
+
phpsessid = pair.split("=", 2).last.to_s.strip
|
|
76
|
+
break
|
|
77
|
+
end
|
|
78
|
+
raise "minuteinbox: 未获取到 PHPSESSID cookie" if phpsessid.empty?
|
|
79
|
+
|
|
80
|
+
create_url = "#{BASE_URL}/index/index?csrf_token=#{URI.encode_uri_component(csrf)}"
|
|
81
|
+
r2 = Http.get(
|
|
82
|
+
create_url,
|
|
83
|
+
headers: hdrs.merge(
|
|
84
|
+
"X-Requested-With" => "XMLHttpRequest",
|
|
85
|
+
"Cookie" => "PHPSESSID=#{phpsessid}"
|
|
86
|
+
),
|
|
87
|
+
timeout: 15
|
|
88
|
+
)
|
|
89
|
+
r2.raise_for_status
|
|
90
|
+
|
|
91
|
+
data = r2.json
|
|
92
|
+
email = data.is_a?(Hash) ? data["email"].to_s.strip : ""
|
|
93
|
+
raise "minuteinbox: 获取到的邮箱地址无效" if email.empty? || !email.include?("@")
|
|
94
|
+
|
|
95
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: encode_session(phpsessid, csrf))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# 获取 minuteinbox.com 邮件列表
|
|
99
|
+
# @param token [String] JSON session token
|
|
100
|
+
# @param email [String]
|
|
101
|
+
# @return [Array<Email>]
|
|
102
|
+
def get_emails(token, email)
|
|
103
|
+
phpsessid, _csrf = decode_session(token)
|
|
104
|
+
cookie_hdr = "PHPSESSID=#{phpsessid}"
|
|
105
|
+
hdrs = {
|
|
106
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
107
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
|
|
108
|
+
"X-Requested-With" => "XMLHttpRequest",
|
|
109
|
+
"Cookie" => cookie_hdr
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
r = Http.get(BASE_URL + "/index/refresh", headers: hdrs, timeout: 15)
|
|
113
|
+
r.raise_for_status
|
|
114
|
+
|
|
115
|
+
trimmed = r.body.to_s.strip
|
|
116
|
+
return [] if trimmed == "0" || trimmed.empty? || trimmed == "[]"
|
|
117
|
+
|
|
118
|
+
mail_list = r.json
|
|
119
|
+
return [] unless mail_list.is_a?(Array) && !mail_list.empty?
|
|
120
|
+
|
|
121
|
+
mail_list.filter_map do |item|
|
|
122
|
+
next unless item.is_a?(Hash)
|
|
123
|
+
|
|
124
|
+
mail_id = item["id"].to_s
|
|
125
|
+
detail = fetch_detail(cookie_hdr, mail_id) unless mail_id.empty?
|
|
126
|
+
|
|
127
|
+
is_read = item["precteno"] != "new"
|
|
128
|
+
from = detail&.dig("od") || item["od"] || ""
|
|
129
|
+
subject = detail&.dig("predmet") || item["predmet"] || ""
|
|
130
|
+
html_body = detail&.dig("telo") || ""
|
|
131
|
+
to = detail&.dig("komu") || email
|
|
132
|
+
|
|
133
|
+
flat = {
|
|
134
|
+
"id" => mail_id,
|
|
135
|
+
"from" => from,
|
|
136
|
+
"to" => to,
|
|
137
|
+
"subject" => subject,
|
|
138
|
+
"html" => html_body,
|
|
139
|
+
"date" => item["kdy"] || "",
|
|
140
|
+
"isRead" => is_read
|
|
141
|
+
}
|
|
142
|
+
Normalize.normalize_email(flat, email)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# 获取单封邮件详情
|
|
147
|
+
# @param cookie_hdr [String]
|
|
148
|
+
# @param mail_id [String]
|
|
149
|
+
# @return [Hash, nil]
|
|
150
|
+
def fetch_detail(cookie_hdr, mail_id)
|
|
151
|
+
return nil if mail_id.empty?
|
|
152
|
+
|
|
153
|
+
hdrs = {
|
|
154
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
155
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
|
|
156
|
+
"X-Requested-With" => "XMLHttpRequest",
|
|
157
|
+
"Content-Type" => "application/x-www-form-urlencoded",
|
|
158
|
+
"Cookie" => cookie_hdr
|
|
159
|
+
}
|
|
160
|
+
resp = Http.post(
|
|
161
|
+
BASE_URL + "/index/email",
|
|
162
|
+
headers: hdrs,
|
|
163
|
+
body: "id=#{URI.encode_uri_component(mail_id)}",
|
|
164
|
+
timeout: 15
|
|
165
|
+
)
|
|
166
|
+
return nil unless resp.ok?
|
|
167
|
+
|
|
168
|
+
resp.json
|
|
169
|
+
rescue StandardError
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -42,7 +42,7 @@ module TempmailSdk
|
|
|
42
42
|
TITLE_RE = /<li\s+class="title"\s*>([^<]*)<\/li>/im
|
|
43
43
|
DATE_RE = /<li\s+class="date"[^>]*>[\s\S]*?<span[^>]*>([^<]+)<\/span>/im
|
|
44
44
|
SENDER_RE = /<li\s+class="sender"[^>]*>[\s\S]*?<span[^>]*>([\s\S]*?)<\/span>\s*<\/li>/im
|
|
45
|
-
|
|
45
|
+
BODY_OPEN_RE = /<div\s+class="email-body"\s*>/im
|
|
46
46
|
FROM_ADDR_RE = /<([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>/
|
|
47
47
|
TAG_RE = /<[^>]+>/
|
|
48
48
|
|
|
@@ -161,6 +161,33 @@ module TempmailSdk
|
|
|
161
161
|
addr
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
+
# 使用栈式深度匹配提取 email-body div 的完整内部 HTML,
|
|
165
|
+
# 避免非贪婪正则在嵌套 div 时截断正文。
|
|
166
|
+
def extract_body_html(page)
|
|
167
|
+
m = page.match(BODY_OPEN_RE)
|
|
168
|
+
return "" unless m
|
|
169
|
+
|
|
170
|
+
start = m.end(0)
|
|
171
|
+
pos = start
|
|
172
|
+
depth = 1
|
|
173
|
+
while pos < page.length && depth > 0
|
|
174
|
+
next_open = page.index("<div", pos)
|
|
175
|
+
next_close = page.index("</div>", pos)
|
|
176
|
+
break unless next_close
|
|
177
|
+
|
|
178
|
+
if next_open && next_open < next_close
|
|
179
|
+
depth += 1
|
|
180
|
+
pos = next_open + 4
|
|
181
|
+
else
|
|
182
|
+
depth -= 1
|
|
183
|
+
return page[start...next_close].strip if depth.zero?
|
|
184
|
+
|
|
185
|
+
pos = next_close + 6
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
""
|
|
189
|
+
end
|
|
190
|
+
|
|
164
191
|
# 去除 HTML 标签,压缩为文本
|
|
165
192
|
def strip_tags(s)
|
|
166
193
|
s.gsub(TAG_RE, " ").strip
|
|
@@ -200,8 +227,7 @@ module TempmailSdk
|
|
|
200
227
|
date_s = ""
|
|
201
228
|
date_s = CGI.unescapeHTML(page.match(DATE_RE)[1].strip) if page.match?(DATE_RE)
|
|
202
229
|
|
|
203
|
-
body =
|
|
204
|
-
body = page.match(BODY_RE)[1].strip if page.match?(BODY_RE)
|
|
230
|
+
body = extract_body_html(page)
|
|
205
231
|
|
|
206
232
|
{
|
|
207
233
|
"id" => mid,
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# mohmal.com 渠道实现(HTML 页面解析,session cookie)
|
|
8
|
+
#
|
|
9
|
+
# 流程:GET /en/create/random 创建邮箱(跟随重定向到 /en/inbox),从 data-email 属性提取邮箱地址
|
|
10
|
+
# GET /en/inbox 解析邮件链接列表
|
|
11
|
+
# GET /en/message/{id}/html 获取邮件详情
|
|
12
|
+
# token 存储 connect.sid cookie 字符串
|
|
13
|
+
# 注意:正文提取使用栈式深度匹配,避免非贪婪正则截断嵌套 div
|
|
14
|
+
module Mohmal
|
|
15
|
+
CHANNEL = "mohmal"
|
|
16
|
+
BASE_URL = "https://www.mohmal.com"
|
|
17
|
+
|
|
18
|
+
DATA_EMAIL_RE = /data-email=["']([^"']+)["']/im
|
|
19
|
+
MSG_HREF_RE = /href=["'](\/en\/message\/([^"']+))["']/i
|
|
20
|
+
DETAIL_FROM_RE = /<span[^>]+class=["'][^"']*from[^"']*["'][^>]*>([\s\S]*?)<\/span>/im
|
|
21
|
+
DETAIL_SUBJECT_RE = /<span[^>]+class=["'][^"']*subject[^"']*["'][^>]*>([\s\S]*?)<\/span>/im
|
|
22
|
+
DETAIL_DATE_RE = /<span[^>]+class=["'][^"']*date[^"']*["'][^>]*>([\s\S]*?)<\/span>/im
|
|
23
|
+
TAG_RE = /<[^>]+>/
|
|
24
|
+
BODY_OPEN_RE = /<div\s+class=["'][^"']*(?:mail-body|message-body|message_body)[^"']*["']\s*>/im
|
|
25
|
+
|
|
26
|
+
BROWSER_HEADERS = {
|
|
27
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
28
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
|
|
29
|
+
"Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif," \
|
|
30
|
+
"image/webp,image/apng,*/*;q=0.8",
|
|
31
|
+
"Accept-Language" => "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
|
|
32
|
+
"Cache-Control" => "no-cache",
|
|
33
|
+
"DNT" => "1",
|
|
34
|
+
"Pragma" => "no-cache",
|
|
35
|
+
"Upgrade-Insecure-Requests" => "1"
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
# 从 HTML 中提取 data-email 属性值
|
|
41
|
+
# @param html [String]
|
|
42
|
+
# @return [String]
|
|
43
|
+
def extract_email(html)
|
|
44
|
+
m = html.match(DATA_EMAIL_RE)
|
|
45
|
+
m ? CGI.unescapeHTML(m[1].strip) : ""
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# 合并 Cookie 头与响应 Set-Cookie
|
|
49
|
+
# @param existing [String]
|
|
50
|
+
# @param set_cookie_lines [Array<String>]
|
|
51
|
+
# @return [String]
|
|
52
|
+
def merge_cookies(existing, set_cookie_lines)
|
|
53
|
+
map = {}
|
|
54
|
+
existing.to_s.split(";").each do |part|
|
|
55
|
+
part = part.strip
|
|
56
|
+
next unless part.include?("=")
|
|
57
|
+
|
|
58
|
+
k, v = part.split("=", 2)
|
|
59
|
+
map[k.strip] = v.to_s.strip unless k.strip.empty?
|
|
60
|
+
end
|
|
61
|
+
set_cookie_lines.each do |line|
|
|
62
|
+
pair = line.split(";", 2).first.to_s.strip
|
|
63
|
+
next unless pair.include?("=")
|
|
64
|
+
|
|
65
|
+
k, v = pair.split("=", 2)
|
|
66
|
+
map[k.strip] = v.to_s.strip unless k.strip.empty?
|
|
67
|
+
end
|
|
68
|
+
map.map { |k, v| "#{k}=#{v}" }.join("; ")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# 去除 HTML 标签并修剪空白
|
|
72
|
+
# @param s [String]
|
|
73
|
+
# @return [String]
|
|
74
|
+
def strip_tags(s)
|
|
75
|
+
s.gsub(TAG_RE, " ").strip
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# 使用栈式深度匹配提取指定 class 的 div 完整内部 HTML
|
|
79
|
+
# 避免非贪婪正则在嵌套 div 时截断正文
|
|
80
|
+
# @param page [String]
|
|
81
|
+
# @return [String]
|
|
82
|
+
def extract_body_html(page)
|
|
83
|
+
m = page.match(BODY_OPEN_RE)
|
|
84
|
+
return "" unless m
|
|
85
|
+
|
|
86
|
+
start = m.end(0)
|
|
87
|
+
pos = start
|
|
88
|
+
depth = 1
|
|
89
|
+
while pos < page.length && depth > 0
|
|
90
|
+
next_open = page.index("<div", pos)
|
|
91
|
+
next_close = page.index("</div>", pos)
|
|
92
|
+
break unless next_close
|
|
93
|
+
|
|
94
|
+
if next_open && next_open < next_close
|
|
95
|
+
depth += 1
|
|
96
|
+
pos = next_open + 4
|
|
97
|
+
else
|
|
98
|
+
depth -= 1
|
|
99
|
+
return page[start...next_close].strip if depth.zero?
|
|
100
|
+
|
|
101
|
+
pos = next_close + 6
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
""
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# 创建 mohmal.com 临时邮箱
|
|
108
|
+
# @return [EmailInfo]
|
|
109
|
+
def generate_email
|
|
110
|
+
r1 = Http.get(
|
|
111
|
+
BASE_URL + "/en/create/random",
|
|
112
|
+
headers: BROWSER_HEADERS.merge("Referer" => BASE_URL + "/en"),
|
|
113
|
+
timeout: 15
|
|
114
|
+
)
|
|
115
|
+
r1.raise_for_status
|
|
116
|
+
cookie_hdr = merge_cookies("", r1.set_cookies)
|
|
117
|
+
email = extract_email(r1.body)
|
|
118
|
+
|
|
119
|
+
if email.empty?
|
|
120
|
+
r2 = Http.get(
|
|
121
|
+
BASE_URL + "/en/inbox",
|
|
122
|
+
headers: BROWSER_HEADERS.merge("Referer" => BASE_URL + "/en", "Cookie" => cookie_hdr),
|
|
123
|
+
timeout: 15
|
|
124
|
+
)
|
|
125
|
+
r2.raise_for_status
|
|
126
|
+
cookie_hdr = merge_cookies(cookie_hdr, r2.set_cookies)
|
|
127
|
+
email = extract_email(r2.body)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
raise "mohmal: 未能从页面中提取邮箱地址" if email.empty?
|
|
131
|
+
|
|
132
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: cookie_hdr)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# 获取 mohmal.com 邮件列表
|
|
136
|
+
# @param email [String]
|
|
137
|
+
# @param token [String] connect.sid cookie 字符串
|
|
138
|
+
# @return [Array<Email>]
|
|
139
|
+
def get_emails(email, token)
|
|
140
|
+
raise "mohmal: session cookie 为空" if token.to_s.strip.empty?
|
|
141
|
+
|
|
142
|
+
r = Http.get(
|
|
143
|
+
BASE_URL + "/en/inbox",
|
|
144
|
+
headers: BROWSER_HEADERS.merge(
|
|
145
|
+
"Referer" => BASE_URL + "/en",
|
|
146
|
+
"Cookie" => token
|
|
147
|
+
),
|
|
148
|
+
timeout: 15
|
|
149
|
+
)
|
|
150
|
+
r.raise_for_status
|
|
151
|
+
|
|
152
|
+
seen = {}
|
|
153
|
+
msgs = []
|
|
154
|
+
r.body.scan(MSG_HREF_RE) do |path, id|
|
|
155
|
+
next if path.include?("/delete")
|
|
156
|
+
next if seen[id]
|
|
157
|
+
|
|
158
|
+
seen[id] = true
|
|
159
|
+
msgs << { path: path, id: id }
|
|
160
|
+
end
|
|
161
|
+
return [] if msgs.empty?
|
|
162
|
+
|
|
163
|
+
msgs.filter_map do |msg|
|
|
164
|
+
raw = fetch_detail(token, msg[:id], email)
|
|
165
|
+
Normalize.normalize_email(raw, email)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# 获取单封邮件详情页
|
|
170
|
+
# @param cookie [String]
|
|
171
|
+
# @param id [String]
|
|
172
|
+
# @param recipient [String]
|
|
173
|
+
# @return [Hash]
|
|
174
|
+
def fetch_detail(cookie, id, recipient)
|
|
175
|
+
raw = { "id" => id, "to" => recipient }
|
|
176
|
+
detail_url = "#{BASE_URL}/en/message/#{id}/html"
|
|
177
|
+
resp = Http.get(
|
|
178
|
+
detail_url,
|
|
179
|
+
headers: BROWSER_HEADERS.merge(
|
|
180
|
+
"Referer" => "#{BASE_URL}/en/inbox",
|
|
181
|
+
"Cookie" => cookie
|
|
182
|
+
),
|
|
183
|
+
timeout: 15
|
|
184
|
+
)
|
|
185
|
+
return raw unless resp.status_code == 200
|
|
186
|
+
|
|
187
|
+
page = resp.body
|
|
188
|
+
|
|
189
|
+
if (m = page.match(DETAIL_FROM_RE))
|
|
190
|
+
raw["from"] = strip_tags(CGI.unescapeHTML(m[1])).strip
|
|
191
|
+
end
|
|
192
|
+
if (m = page.match(DETAIL_SUBJECT_RE))
|
|
193
|
+
raw["subject"] = CGI.unescapeHTML(strip_tags(m[1])).strip
|
|
194
|
+
end
|
|
195
|
+
if (m = page.match(DETAIL_DATE_RE))
|
|
196
|
+
raw["date"] = CGI.unescapeHTML(strip_tags(m[1])).strip
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
body_html = extract_body_html(page)
|
|
200
|
+
raw["html"] = body_html unless body_html.empty?
|
|
201
|
+
|
|
202
|
+
raw
|
|
203
|
+
rescue StandardError
|
|
204
|
+
raw
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|