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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/tempmail_sdk/providers/altmails.rb +99 -0
  3. data/lib/tempmail_sdk/providers/apihz.rb +138 -0
  4. data/lib/tempmail_sdk/providers/awamail.rb +95 -0
  5. data/lib/tempmail_sdk/providers/best_temp_mail.rb +77 -0
  6. data/lib/tempmail_sdk/providers/chatgpt_org_uk.rb +129 -0
  7. data/lib/tempmail_sdk/providers/disposablemail.rb +125 -0
  8. data/lib/tempmail_sdk/providers/disposablemail_app.rb +66 -0
  9. data/lib/tempmail_sdk/providers/email10min.rb +152 -0
  10. data/lib/tempmail_sdk/providers/emailnator.rb +132 -0
  11. data/lib/tempmail_sdk/providers/emailtemp_org.rb +131 -0
  12. data/lib/tempmail_sdk/providers/expressinboxhub.rb +118 -0
  13. data/lib/tempmail_sdk/providers/fakemail.rb +143 -0
  14. data/lib/tempmail_sdk/providers/haribu.rb +29 -3
  15. data/lib/tempmail_sdk/providers/linshiyouxiang_net.rb +85 -0
  16. data/lib/tempmail_sdk/providers/mail_sunls.rb +42 -1
  17. data/lib/tempmail_sdk/providers/mail_td.rb +161 -0
  18. data/lib/tempmail_sdk/providers/mailcat_ai.rb +52 -0
  19. data/lib/tempmail_sdk/providers/maildrop.rb +79 -4
  20. data/lib/tempmail_sdk/providers/mailgolem.rb +96 -0
  21. data/lib/tempmail_sdk/providers/mailinator.rb +9 -2
  22. data/lib/tempmail_sdk/providers/mailtemp_cc.rb +91 -0
  23. data/lib/tempmail_sdk/providers/minuteinbox.rb +174 -0
  24. data/lib/tempmail_sdk/providers/moakt.rb +29 -3
  25. data/lib/tempmail_sdk/providers/mohmal.rb +208 -0
  26. data/lib/tempmail_sdk/providers/mytempmail_cc.rb +76 -0
  27. data/lib/tempmail_sdk/providers/openinbox.rb +102 -0
  28. data/lib/tempmail_sdk/providers/smail_pw.rb +156 -0
  29. data/lib/tempmail_sdk/providers/socketio_mail.rb +25 -0
  30. data/lib/tempmail_sdk/providers/tempgbox.rb +118 -0
  31. data/lib/tempmail_sdk/providers/tempmail_cn.rb +111 -0
  32. data/lib/tempmail_sdk/providers/tenminutemail_net.rb +183 -0
  33. data/lib/tempmail_sdk/providers/twentyfourmail_chacuo.rb +83 -0
  34. data/lib/tempmail_sdk/providers/vip_215.rb +171 -0
  35. data/lib/tempmail_sdk/registry.rb +163 -1
  36. data/lib/tempmail_sdk/version.rb +1 -1
  37. 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" => text_from_payload(text_payload, "text/plain"),
145
- "html" => text_from_payload(html_payload, "text/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
- BODY_RE = /<div\s+class="email-body"\s*>([\s\S]*?)<\/div>/im
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