tempmail_sdk 1.3.3 → 1.3.5

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/tempmail_sdk/channels.rb +29 -7
  4. data/lib/tempmail_sdk/channels_data.rb +29 -7
  5. data/lib/tempmail_sdk/providers/altmails.rb +99 -0
  6. data/lib/tempmail_sdk/providers/apihz.rb +138 -0
  7. data/lib/tempmail_sdk/providers/awamail.rb +95 -0
  8. data/lib/tempmail_sdk/providers/best_temp_mail.rb +77 -0
  9. data/lib/tempmail_sdk/providers/chatgpt_org_uk.rb +129 -0
  10. data/lib/tempmail_sdk/providers/clawdemail.rb +122 -0
  11. data/lib/tempmail_sdk/providers/crazymailing.rb +114 -0
  12. data/lib/tempmail_sdk/providers/disposablemail.rb +125 -0
  13. data/lib/tempmail_sdk/providers/disposablemail_app.rb +66 -0
  14. data/lib/tempmail_sdk/providers/email10min.rb +152 -0
  15. data/lib/tempmail_sdk/providers/emailnator.rb +132 -0
  16. data/lib/tempmail_sdk/providers/emailtemp_org.rb +131 -0
  17. data/lib/tempmail_sdk/providers/expressinboxhub.rb +118 -0
  18. data/lib/tempmail_sdk/providers/fakemail.rb +143 -0
  19. data/lib/tempmail_sdk/providers/firetempmail.rb +86 -0
  20. data/lib/tempmail_sdk/providers/flybymail.rb +93 -0
  21. data/lib/tempmail_sdk/providers/generator_email.rb +146 -0
  22. data/lib/tempmail_sdk/providers/haribu.rb +29 -3
  23. data/lib/tempmail_sdk/providers/huskmail.rb +132 -0
  24. data/lib/tempmail_sdk/providers/internxt.rb +176 -0
  25. data/lib/tempmail_sdk/providers/linshi_xyz.rb +79 -0
  26. data/lib/tempmail_sdk/providers/linshiyouxiang_net.rb +85 -0
  27. data/lib/tempmail_sdk/providers/mail_sunls.rb +42 -1
  28. data/lib/tempmail_sdk/providers/mail_td.rb +161 -0
  29. data/lib/tempmail_sdk/providers/mailcat_ai.rb +52 -0
  30. data/lib/tempmail_sdk/providers/maildrop.rb +79 -4
  31. data/lib/tempmail_sdk/providers/mailgolem.rb +96 -0
  32. data/lib/tempmail_sdk/providers/mailinator.rb +9 -2
  33. data/lib/tempmail_sdk/providers/mailmomy.rb +1 -1
  34. data/lib/tempmail_sdk/providers/mailtemp_cc.rb +91 -0
  35. data/lib/tempmail_sdk/providers/mailticking.rb +139 -0
  36. data/lib/tempmail_sdk/providers/minuteinbox.rb +174 -0
  37. data/lib/tempmail_sdk/providers/moakt.rb +29 -3
  38. data/lib/tempmail_sdk/providers/mtempmail.rb +140 -0
  39. data/lib/tempmail_sdk/providers/mytempmail_cc.rb +76 -0
  40. data/lib/tempmail_sdk/providers/nowtempmail.rb +112 -0
  41. data/lib/tempmail_sdk/providers/noxen_de5_net.rb +392 -0
  42. data/lib/tempmail_sdk/providers/nukemail.rb +188 -0
  43. data/lib/tempmail_sdk/providers/nullmail.rb +95 -0
  44. data/lib/tempmail_sdk/providers/openinbox.rb +102 -0
  45. data/lib/tempmail_sdk/providers/shadowmail.rb +174 -0
  46. data/lib/tempmail_sdk/providers/shitpost_email.rb +85 -0
  47. data/lib/tempmail_sdk/providers/smail_pw.rb +156 -0
  48. data/lib/tempmail_sdk/providers/smails.rb +128 -0
  49. data/lib/tempmail_sdk/providers/socketio_mail.rb +25 -0
  50. data/lib/tempmail_sdk/providers/temp_mail_gg.rb +362 -0
  51. data/lib/tempmail_sdk/providers/tempgbox.rb +118 -0
  52. data/lib/tempmail_sdk/providers/tempmail100.rb +148 -0
  53. data/lib/tempmail_sdk/providers/tempmail_cn.rb +111 -0
  54. data/lib/tempmail_sdk/providers/tempmail_ee.rb +373 -0
  55. data/lib/tempmail_sdk/providers/tempmailportal.rb +131 -0
  56. data/lib/tempmail_sdk/providers/tempmails_io.rb +89 -0
  57. data/lib/tempmail_sdk/providers/tempmailto.rb +312 -0
  58. data/lib/tempmail_sdk/providers/temporarymail_com.rb +184 -0
  59. data/lib/tempmail_sdk/providers/tenmin_app.rb +100 -0
  60. data/lib/tempmail_sdk/providers/tenminutemail_net.rb +183 -0
  61. data/lib/tempmail_sdk/providers/thirty_min_email.rb +90 -0
  62. data/lib/tempmail_sdk/providers/tmpkit.rb +129 -0
  63. data/lib/tempmail_sdk/providers/twentyfourmail_chacuo.rb +83 -0
  64. data/lib/tempmail_sdk/providers/vip_215.rb +171 -0
  65. data/lib/tempmail_sdk/providers/zerodrop.rb +90 -0
  66. data/lib/tempmail_sdk/registry.rb +285 -23
  67. data/lib/tempmail_sdk/types.rb +13 -0
  68. data/lib/tempmail_sdk/version.rb +1 -1
  69. data/lib/tempmail_sdk.rb +1 -1
  70. metadata +58 -6
  71. data/lib/tempmail_sdk/providers/gonebox_email.rb +0 -89
  72. data/lib/tempmail_sdk/providers/temp_mail_now.rb +0 -103
  73. data/lib/tempmail_sdk/providers/webmailtemp.rb +0 -108
@@ -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,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TempmailSdk
4
+ module Providers
5
+ # Mailticking 渠道实现(www.mailticking.com,旧域名 temporary-mail.net 的更名站)
6
+ #
7
+ # 实测协议(三类请求均以 application/json 交流,站点在 Cloudflare 后):
8
+ # 1. 建箱 POST /get-mailbox body {"types":["4"]}(4=独立域名,排除 Gmail 别名)
9
+ # 响应 {"success":true,"email":"xxx@domain","activate_token":"..."}
10
+ # 2. 激活 POST /activate-email body {"email":..,"source":"api","activate_token":..}
11
+ # 响应 {"success":true}
12
+ # 3. 列信 POST /get-emails?lang=en body {"email":..,"code":..}
13
+ # 空箱实测响应 {"emails":[],"success":true};空闲超时/被改绑后返回
14
+ # {"success":false,"needNewEmail":true,...},此时返回错误提示换箱。
15
+ #
16
+ # 读信正文无公开端点:GetEmails 只具备列表能力,列表字段名采用多候选提取
17
+ # 策略,命中多少映射多少(Normalize 既有候选字段负责兜底)。
18
+ module Mailticking
19
+ CHANNEL = "mailticking"
20
+ BASE_URL = "https://www.mailticking.com"
21
+
22
+ HEADERS = {
23
+ "Accept" => "application/json",
24
+ "Accept-Language" => "en-US,en;q=0.9",
25
+ "Content-Type" => "application/json",
26
+ "Referer" => "#{BASE_URL}/",
27
+ "Origin" => BASE_URL,
28
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \
29
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
30
+ }.freeze
31
+
32
+ module_function
33
+
34
+ # 执行 JSON POST 请求并解析响应;响应非 2xx 或 success=false 时抛错
35
+ # @param path [String] 请求路径
36
+ # @param payload [Hash] JSON 请求体
37
+ # @return [Hash]
38
+ def do_post(path, payload)
39
+ resp = Http.post("#{BASE_URL}#{path}", headers: HEADERS, json: payload, timeout: 15)
40
+ data = resp.json
41
+ unless resp.ok?
42
+ msg = data.is_a?(Hash) ? (data["error"] || data["message"]).to_s : ""
43
+ msg = resp.body.to_s if msg.empty?
44
+ raise "mailticking: http #{resp.status_code}: #{msg}"
45
+ end
46
+ unless data.is_a?(Hash) && data["success"] == true
47
+ msg = data.is_a?(Hash) ? (data["error"] || data["message"]).to_s : ""
48
+ msg = "unknown error" if msg.empty?
49
+ raise "mailticking: 请求失败: #{msg}"
50
+ end
51
+ data
52
+ end
53
+
54
+ # 创建 mailticking 邮箱账号
55
+ # 请求 type=4(独立域名)固定取独立域名邮箱,建箱后立即激活会话;
56
+ # token 必须携带 activate_token(列信协议依赖激活会话),不能为空。
57
+ # @return [EmailInfo]
58
+ def generate_email
59
+ box = do_post("/get-mailbox", { "types" => ["4"] })
60
+ token = box["code"].to_s.strip
61
+ token = box["email"].to_s.strip if token.empty?
62
+ email = box["email"].to_s.strip
63
+ if token.empty? || email.empty?
64
+ raise "mailticking: get-mailbox 返回空 email/activate_token"
65
+ end
66
+
67
+ # 激活邮箱,使后续列信请求与服务器记录的最新会话一致
68
+ do_post("/activate-email", {
69
+ "email" => email,
70
+ "source" => "api",
71
+ "activate_token" => token
72
+ })
73
+
74
+ EmailInfo.new(channel: CHANNEL, email: email, token: token)
75
+ end
76
+
77
+ # 获取 mailticking 邮箱的邮件列表
78
+ # 空箱返回空列表不报错;需要换箱的响应(needNewEmail)返回语义错误。
79
+ # @param email [String] 邮箱地址
80
+ # @param token [String] 建箱返回的激活令牌
81
+ # @return [Array<Email>]
82
+ def get_emails(email, token)
83
+ addr = email.to_s.strip
84
+ tk = token.to_s.strip
85
+ raise "mailticking: 邮箱地址为空" if addr.empty?
86
+ raise "mailticking: activate code 为空" if tk.empty?
87
+
88
+ resp = Http.post("#{BASE_URL}/get-emails?lang=en",
89
+ headers: HEADERS,
90
+ json: { "email" => addr, "code" => tk },
91
+ timeout: 15)
92
+ data = resp.json
93
+ unless resp.ok?
94
+ msg = data.is_a?(Hash) ? (data["error"] || data["message"]).to_s : ""
95
+ msg = resp.body.to_s if msg.empty?
96
+ raise "mailticking: http #{resp.status_code}: #{msg}"
97
+ end
98
+ unless data.is_a?(Hash) && data["success"] == true
99
+ raise "mailticking: 邮箱已过期,请重新建箱" if data.is_a?(Hash) && data["needNewEmail"] == true
100
+
101
+ raise "mailticking: get-emails 失败"
102
+ end
103
+ emails = data["emails"]
104
+ return [] unless emails.is_a?(Array)
105
+
106
+ emails.filter_map do |raw|
107
+ next unless raw.is_a?(Hash)
108
+
109
+ Normalize.normalize_email(migrated_fields(raw), addr)
110
+ end
111
+ end
112
+
113
+ # 尽量映射列表字段到统一字段名
114
+ # 官网首页表格只有 SENDER/SUBJECT/TIME 三列,字段全名缺少可观测证据,
115
+ # 因此对常见字段做多候选提取;未命中的字段留给 Normalize 自己处理。
116
+ # @param raw [Hash]
117
+ # @return [Hash]
118
+ def migrated_fields(raw)
119
+ m = raw.dup
120
+ unless m.key?("from")
121
+ %w[mail_from from_mail from_email sender_address from_address
122
+ send_addr mail_addr address_from ho_from fromname fromS].each do |key|
123
+ v = m[key]
124
+ if v.is_a?(String) && v.strip != ""
125
+ m["from"] = v
126
+ break
127
+ end
128
+ end
129
+ end
130
+ if !m.key?("sender") && m["from"].is_a?(String) && m["from"].strip != ""
131
+ m["sender"] = m["from"]
132
+ end
133
+ m["id"] = m["mail_id"] if !m.key?("id") && m.key?("mail_id")
134
+ m["date"] = m["received_at"] if !m.key?("date") && m.key?("received_at")
135
+ m
136
+ end
137
+ end
138
+ end
139
+ 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,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # Mtempmail 渠道实现(mtempmail.com,公共 key 认证)
8
+ #
9
+ # 建箱: POST /api/emails/{apiKey}(空 JSON body)→
10
+ # {"status":true,"data":{"email":"...@domain","domain":"..","ip":"..",
11
+ # "fingerprint":"..","expire_at":"..","created_at":"..","id":..,"email_token":"..."}}
12
+ # 读信: GET /api/messages/{apiKey}/{email} →
13
+ # {"status":true,"mailbox":"..","email_token":"..","messages":[...]}
14
+ # 消息为 mailgun 入站 webhook 风格:to/from 为对象数组,
15
+ # body 为 [{content_type:"text/html",value:".."},...] 分段。
16
+ # 邮箱 24 小时有效(过期时间标注不一致,以服务端为准)。
17
+ module Mtempmail
18
+ CHANNEL = "mtempmail"
19
+ BASE_URL = "https://mtempmail.com"
20
+
21
+ # 公共固定 API key(mtempmail.com 官方提供)
22
+ PUBLIC_KEY = "pub_nRn1hUwpdmZvxQNVWDfoXgKyF7dIm9nRIIIt1qDw"
23
+
24
+ HEADERS = {
25
+ "Accept" => "application/json",
26
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
27
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
28
+ }.freeze
29
+
30
+ # 剥离后台拼接的 "• " 前缀(或实收分隔符)
31
+ BULLET_RE = /^[•·]+\s*/.freeze
32
+
33
+ module_function
34
+
35
+ # 清洗主题前导分隔符(后台可能以 "• " 开头拼接微件)
36
+ # @param subject [String]
37
+ # @return [String]
38
+ def clean_subject(subject)
39
+ subject.to_s.strip.sub(BULLET_RE, "")
40
+ end
41
+
42
+ # 拼接正文纯文本(body[].value 按序)
43
+ # @param parts [Array]
44
+ # @return [String]
45
+ def body_text(parts)
46
+ parts.filter_map { |p| p["value"].to_s unless p.nil? || !p.is_a?(Hash) || p["value"].to_s.empty? }
47
+ .join("\n")
48
+ end
49
+
50
+ # 提取首个 text/html 段
51
+ # @param parts [Array]
52
+ # @return [String]
53
+ def body_html(parts)
54
+ parts.each do |p|
55
+ next unless p.is_a?(Hash) && p["content_type"].to_s == "text/html"
56
+
57
+ val = p["value"].to_s
58
+ return val unless val.empty?
59
+ end
60
+ ""
61
+ end
62
+
63
+ # 创建 mtempmail 临时邮箱
64
+ # POST /api/emails/{apiKey}(空 JSON body)
65
+ # @return [EmailInfo]
66
+ def generate_email
67
+ resp = Http.post("#{BASE_URL}/api/emails/#{PUBLIC_KEY}",
68
+ headers: HEADERS.merge("Content-Type" => "application/json"),
69
+ json: {}, timeout: 15)
70
+ resp.raise_for_status
71
+ data = resp.json
72
+ payload = data.is_a?(Hash) ? data["data"] : nil
73
+ email = payload.is_a?(Hash) ? payload["email"].to_s.strip : ""
74
+ if (data.is_a?(Hash) && !data["status"]) || email.empty?
75
+ raise "mtempmail: 建箱响应缺少邮箱"
76
+ end
77
+
78
+ EmailInfo.new(channel: CHANNEL, email: email,
79
+ token: payload["email_token"].to_s,
80
+ expires_at: payload["expire_at"],
81
+ created_at: payload["created_at"])
82
+ end
83
+
84
+ # 读取 mtempmail 收件箱
85
+ # GET /api/messages/{apiKey}/{email};token 元数据仅为校验,不参与请求
86
+ # @param email [String] 邮箱地址
87
+ # @param token [String] email_token(仅校验非空)
88
+ # @return [Array<Email>]
89
+ def get_emails(email, token)
90
+ addr = email.to_s.strip
91
+ raise "mtempmail: 邮箱为空" if addr.empty?
92
+ raise "mtempmail: token 为空" if token.to_s.strip.empty?
93
+
94
+ uri = "#{BASE_URL}/api/messages/#{PUBLIC_KEY}/#{URI.encode_www_form_component(addr)}"
95
+ resp = Http.get(uri, headers: HEADERS, timeout: 15)
96
+ resp.raise_for_status
97
+ data = resp.json
98
+ messages = data.is_a?(Hash) ? data["messages"] : nil
99
+ return [] unless messages.is_a?(Array)
100
+
101
+ messages.filter_map do |m|
102
+ next unless m.is_a?(Hash)
103
+
104
+ to_str = flatten_contact(m["to"])
105
+ row = {
106
+ "id" => m["id"],
107
+ "from" => flatten_contact(m["from"]),
108
+ "to" => to_str.empty? ? addr : to_str,
109
+ "subject" => clean_subject(m["subject"]),
110
+ "date" => m["created_at"] || m["date"]
111
+ }
112
+ body_parts = m["body"]
113
+ if body_parts.is_a?(Array)
114
+ parts = body_parts.select { |p| p.is_a?(Hash) }
115
+ text = body_text(parts)
116
+ row["text"] = text unless text.empty?
117
+ html_part = body_html(parts)
118
+ row["html"] = html_part unless html_part.empty?
119
+ end
120
+ Normalize.normalize_email(row, addr)
121
+ end
122
+ end
123
+
124
+ # 将 mailgun 风格 [{"full":"Sender <a@b.com>"},{...}] 展平为字符串
125
+ # 优先取 full 字段,否则回退 address;空列表返回空串
126
+ # @param contacts [Object]
127
+ # @return [String]
128
+ def flatten_contact(contacts)
129
+ return contacts.to_s unless contacts.is_a?(Array)
130
+ return "" if contacts.empty?
131
+
132
+ first = contacts.first
133
+ return "" unless first.is_a?(Hash)
134
+
135
+ full = first["full"].to_s.strip
136
+ full.empty? ? first["address"].to_s.strip : full
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # mytempmail.cc 渠道实现
8
+ #
9
+ # 流程:POST /api/address 创建邮箱(JSON body),返回 token + address
10
+ # GET /api/mails/{token} 获取邮件列表
11
+ # 域名固定 nilvaro.com,有效期 600 秒
12
+ module MytempmailCc
13
+ CHANNEL = "mytempmail-cc"
14
+ BASE_URL = "https://api.mytempmail.cc"
15
+
16
+ HEADERS = {
17
+ "Content-Type" => "application/json",
18
+ "Accept" => "application/json"
19
+ }.freeze
20
+
21
+ CHARS = ("a".."z").to_a.freeze
22
+
23
+ module_function
24
+
25
+ # 生成随机 8 位小写字母用户名
26
+ # @return [String]
27
+ def random_name
28
+ Array.new(8) { CHARS.sample }.join
29
+ end
30
+
31
+ # 创建 mytempmail.cc 临时邮箱
32
+ # @return [EmailInfo]
33
+ def generate_email
34
+ body = JSON.generate("domain" => "nilvaro.com", "name" => random_name, "expiry" => 600)
35
+ resp = Http.post(BASE_URL + "/api/address", headers: HEADERS, body: body, timeout: 15)
36
+ resp.raise_for_status
37
+ data = resp.json
38
+ email = data["address"].to_s.strip
39
+ token = data["token"].to_s.strip
40
+ raise "mytempmail-cc: 无效的邮箱响应" if email.empty? || !email.include?("@")
41
+ raise "mytempmail-cc: 响应中缺少 token" if token.empty?
42
+
43
+ EmailInfo.new(channel: CHANNEL, email: email, token: token)
44
+ end
45
+
46
+ # 获取 mytempmail.cc 邮件列表
47
+ # @param token [String]
48
+ # @param email [String]
49
+ # @return [Array<Email>]
50
+ def get_emails(token, email)
51
+ token = token.to_s.strip
52
+ raise "mytempmail-cc: token 为空" if token.empty?
53
+
54
+ resp = Http.get("#{BASE_URL}/api/mails/#{token}", headers: HEADERS, timeout: 15)
55
+ resp.raise_for_status
56
+ data = resp.json
57
+ results = data.is_a?(Hash) ? Array(data["results"]) : []
58
+
59
+ results.filter_map do |msg|
60
+ next unless msg.is_a?(Hash)
61
+
62
+ flat = {
63
+ "id" => msg["id"],
64
+ "from" => msg["from"],
65
+ "to" => email,
66
+ "subject" => msg["subject"],
67
+ "text" => msg["body_text"],
68
+ "html" => msg["body_html"],
69
+ "received_at" => msg["received_at"]
70
+ }
71
+ Normalize.normalize_email(flat, email)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end