tempmail_sdk 1.3.3

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 (100) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +116 -0
  3. data/lib/tempmail_sdk/channels.rb +289 -0
  4. data/lib/tempmail_sdk/channels_data.rb +289 -0
  5. data/lib/tempmail_sdk/client.rb +193 -0
  6. data/lib/tempmail_sdk/config.rb +104 -0
  7. data/lib/tempmail_sdk/html_utils.rb +42 -0
  8. data/lib/tempmail_sdk/http.rb +225 -0
  9. data/lib/tempmail_sdk/logger.rb +38 -0
  10. data/lib/tempmail_sdk/normalize.rb +193 -0
  11. data/lib/tempmail_sdk/providers/anonbox.rb +248 -0
  12. data/lib/tempmail_sdk/providers/anonymmail.rb +92 -0
  13. data/lib/tempmail_sdk/providers/byom.rb +48 -0
  14. data/lib/tempmail_sdk/providers/catchmail.rb +110 -0
  15. data/lib/tempmail_sdk/providers/cleantempmail.rb +49 -0
  16. data/lib/tempmail_sdk/providers/devmail_uk.rb +58 -0
  17. data/lib/tempmail_sdk/providers/dropmail.rb +133 -0
  18. data/lib/tempmail_sdk/providers/dropmail_click.rb +81 -0
  19. data/lib/tempmail_sdk/providers/dropmail_me.rb +163 -0
  20. data/lib/tempmail_sdk/providers/duckmail.rb +63 -0
  21. data/lib/tempmail_sdk/providers/eyepaste.rb +119 -0
  22. data/lib/tempmail_sdk/providers/fake_email_site.rb +48 -0
  23. data/lib/tempmail_sdk/providers/fake_legal.rb +109 -0
  24. data/lib/tempmail_sdk/providers/fmail.rb +87 -0
  25. data/lib/tempmail_sdk/providers/freecustom.rb +135 -0
  26. data/lib/tempmail_sdk/providers/getnada.rb +146 -0
  27. data/lib/tempmail_sdk/providers/gonebox_email.rb +89 -0
  28. data/lib/tempmail_sdk/providers/guerrillamail.rb +62 -0
  29. data/lib/tempmail_sdk/providers/guerrillamail_mirrors.rb +81 -0
  30. data/lib/tempmail_sdk/providers/harakirimail.rb +84 -0
  31. data/lib/tempmail_sdk/providers/haribu.rb +176 -0
  32. data/lib/tempmail_sdk/providers/inboxes.rb +110 -0
  33. data/lib/tempmail_sdk/providers/inboxkitten.rb +90 -0
  34. data/lib/tempmail_sdk/providers/linshiyou.rb +157 -0
  35. data/lib/tempmail_sdk/providers/lroid.rb +170 -0
  36. data/lib/tempmail_sdk/providers/m2u.rb +126 -0
  37. data/lib/tempmail_sdk/providers/mail10s.rb +59 -0
  38. data/lib/tempmail_sdk/providers/mail123.rb +82 -0
  39. data/lib/tempmail_sdk/providers/mail_cx.rb +133 -0
  40. data/lib/tempmail_sdk/providers/mail_sunls.rb +68 -0
  41. data/lib/tempmail_sdk/providers/mail_tm.rb +99 -0
  42. data/lib/tempmail_sdk/providers/mailcatch.rb +91 -0
  43. data/lib/tempmail_sdk/providers/maildrop.rb +92 -0
  44. data/lib/tempmail_sdk/providers/maildrop_cc.rb +110 -0
  45. data/lib/tempmail_sdk/providers/mailforspam.rb +96 -0
  46. data/lib/tempmail_sdk/providers/mailhole_de.rb +70 -0
  47. data/lib/tempmail_sdk/providers/mailinator.rb +182 -0
  48. data/lib/tempmail_sdk/providers/mailmomy.rb +108 -0
  49. data/lib/tempmail_sdk/providers/mailnesia.rb +141 -0
  50. data/lib/tempmail_sdk/providers/mffac.rb +101 -0
  51. data/lib/tempmail_sdk/providers/moakt.rb +296 -0
  52. data/lib/tempmail_sdk/providers/neighbours.rb +140 -0
  53. data/lib/tempmail_sdk/providers/neighbours_sh.rb +117 -0
  54. data/lib/tempmail_sdk/providers/nimail.rb +100 -0
  55. data/lib/tempmail_sdk/providers/ockito.rb +163 -0
  56. data/lib/tempmail_sdk/providers/one_sec_mail.rb +46 -0
  57. data/lib/tempmail_sdk/providers/restmail_net.rb +60 -0
  58. data/lib/tempmail_sdk/providers/rootsh.rb +167 -0
  59. data/lib/tempmail_sdk/providers/shitty_email.rb +84 -0
  60. data/lib/tempmail_sdk/providers/smailpro.rb +103 -0
  61. data/lib/tempmail_sdk/providers/ta_easy.rb +53 -0
  62. data/lib/tempmail_sdk/providers/temp_mail_io.rb +67 -0
  63. data/lib/tempmail_sdk/providers/temp_mail_now.rb +103 -0
  64. data/lib/tempmail_sdk/providers/temp_mail_org.rb +89 -0
  65. data/lib/tempmail_sdk/providers/tempemail_co.rb +142 -0
  66. data/lib/tempmail_sdk/providers/tempemail_info.rb +121 -0
  67. data/lib/tempmail_sdk/providers/tempemails_net.rb +159 -0
  68. data/lib/tempmail_sdk/providers/tempfastmail.rb +76 -0
  69. data/lib/tempmail_sdk/providers/tempgmailer.rb +126 -0
  70. data/lib/tempmail_sdk/providers/tempgo_email.rb +74 -0
  71. data/lib/tempmail_sdk/providers/tempinbox.rb +70 -0
  72. data/lib/tempmail_sdk/providers/tempmail.rb +66 -0
  73. data/lib/tempmail_sdk/providers/tempmail365.rb +119 -0
  74. data/lib/tempmail_sdk/providers/tempmail_fish.rb +89 -0
  75. data/lib/tempmail_sdk/providers/tempmail_fyi.rb +124 -0
  76. data/lib/tempmail_sdk/providers/tempmail_lol.rb +42 -0
  77. data/lib/tempmail_sdk/providers/tempmail_lol_v2.rb +57 -0
  78. data/lib/tempmail_sdk/providers/tempmail_plus.rb +80 -0
  79. data/lib/tempmail_sdk/providers/tempmailc.rb +80 -0
  80. data/lib/tempmail_sdk/providers/tempmailpro.rb +89 -0
  81. data/lib/tempmail_sdk/providers/tempmailten.rb +150 -0
  82. data/lib/tempmail_sdk/providers/temporam.rb +95 -0
  83. data/lib/tempmail_sdk/providers/tempp_mails.rb +150 -0
  84. data/lib/tempmail_sdk/providers/tempy_email.rb +73 -0
  85. data/lib/tempmail_sdk/providers/ten_minute_mail_net.rb +125 -0
  86. data/lib/tempmail_sdk/providers/tenminute_one.rb +255 -0
  87. data/lib/tempmail_sdk/providers/throwawaymail.rb +80 -0
  88. data/lib/tempmail_sdk/providers/tmail_link.rb +98 -0
  89. data/lib/tempmail_sdk/providers/uncorreotemporal.rb +84 -0
  90. data/lib/tempmail_sdk/providers/webmailtemp.rb +108 -0
  91. data/lib/tempmail_sdk/providers/xkx_me.rb +125 -0
  92. data/lib/tempmail_sdk/providers/zhujump.rb +256 -0
  93. data/lib/tempmail_sdk/registry.rb +1097 -0
  94. data/lib/tempmail_sdk/retry.rb +91 -0
  95. data/lib/tempmail_sdk/telemetry.rb +134 -0
  96. data/lib/tempmail_sdk/types.rb +164 -0
  97. data/lib/tempmail_sdk/version.rb +6 -0
  98. data/lib/tempmail_sdk/webui.rb +360 -0
  99. data/lib/tempmail_sdk.rb +76 -0
  100. metadata +142 -0
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # Mailinator 渠道实现 -- https://mailinator.com
8
+ #
9
+ # 匿名公共信箱:邮箱本地部分即为信箱名,收信统一走
10
+ # mailinator.com 的 public 域名 REST API。众多姊妹域名(如
11
+ # blackhole.djurby.se、sink.fblay.com 等)本质是同一公共信箱的别名,
12
+ # 仅生成邮箱时域名不同,取信逻辑完全一致。
13
+ module Mailinator
14
+ CHANNEL = "mailinator"
15
+ BASE_URL = "https://mailinator.com"
16
+ # 收信统一使用的公共域名标识
17
+ PUBLIC_DOMAIN = "public"
18
+ # 默认生成邮箱所用域名
19
+ DEFAULT_DOMAIN = "mailinator.com"
20
+
21
+ HEADERS = {
22
+ "Accept" => "application/json",
23
+ "Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
24
+ "Cache-Control" => "no-cache",
25
+ "Pragma" => "no-cache",
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
+ module_function
31
+
32
+ # 生成随机本地部分:length 位小写字母数字
33
+ def random_string(length)
34
+ chars = ("a".."z").to_a + ("0".."9").to_a
35
+ Array.new(length) { chars.sample }.join
36
+ end
37
+
38
+ # 生成临时邮箱
39
+ # @param domain [String, nil] 邮箱域名(姊妹渠道派生用),缺省 mailinator.com
40
+ # @param channel [String] 渠道标识
41
+ # @return [EmailInfo]
42
+ def generate_email(domain = nil, channel = CHANNEL)
43
+ dom = domain.to_s.strip
44
+ dom = DEFAULT_DOMAIN if dom.empty?
45
+ EmailInfo.new(channel: channel, email: "#{random_string(12)}@#{dom}")
46
+ end
47
+
48
+ # 拉取邮件列表并逐封获取正文/附件
49
+ # @param token [String] 未使用(公共信箱无需鉴权)
50
+ # @param email [String] 邮箱地址
51
+ # @return [Array<Email>]
52
+ def get_emails(token, email = "")
53
+ _ = token
54
+ inbox = email.to_s.strip
55
+ inbox = inbox.split("@", 2).first.to_s if inbox.include?("@")
56
+ return [] if inbox.empty?
57
+
58
+ data = request_json("#{BASE_URL}/api/v2/domains/#{PUBLIC_DOMAIN}/inboxes/#{inbox}")
59
+ messages = parse_messages(data)
60
+ return [] if messages.empty?
61
+
62
+ messages.map do |msg|
63
+ message_id = (msg["id"] || msg["messageId"]).to_s.strip
64
+ text_payload = message_id.empty? ? nil : request_json(message_path(message_id, "text"))
65
+ html_payload = message_id.empty? ? nil : request_json(message_path(message_id, "texthtml"))
66
+ att_payload = message_id.empty? ? nil : request_json(message_path(message_id, "attachments"))
67
+ Normalize.normalize_email(
68
+ flatten_message(msg, email, text_payload, html_payload, att_payload), email
69
+ )
70
+ end
71
+ end
72
+
73
+ # 拼接单封邮件某子资源的 URL
74
+ def message_path(message_id, suffix)
75
+ "#{BASE_URL}/api/v2/domains/#{PUBLIC_DOMAIN}/messages/#{message_id}/#{suffix}"
76
+ end
77
+
78
+ # 发起 GET 并解析 JSON,失败返回 nil;非 Hash 结果包裹为 { "data" => ... }
79
+ def request_json(url)
80
+ resp = Http.get(url, headers: HEADERS, timeout: 15)
81
+ return nil unless resp.ok?
82
+
83
+ data = resp.json
84
+ data.is_a?(Hash) ? data : { "data" => data }
85
+ rescue StandardError
86
+ nil
87
+ end
88
+
89
+ # 从响应中解析消息数组(兼容顶层数组或 msgs/data 字段)
90
+ def parse_messages(data)
91
+ return data.select { |item| item.is_a?(Hash) } if data.is_a?(Array)
92
+
93
+ if data.is_a?(Hash)
94
+ %w[msgs data].each do |key|
95
+ value = data[key]
96
+ return value.select { |item| item.is_a?(Hash) } if value.is_a?(Array)
97
+ end
98
+ end
99
+ []
100
+ end
101
+
102
+ # 从 payload 中取指定 content-type 的文本
103
+ def text_from_payload(payload, key)
104
+ return "" unless payload.is_a?(Hash)
105
+
106
+ value = payload[key]
107
+ return "" if value.nil?
108
+
109
+ value.is_a?(String) ? value : value.to_s
110
+ end
111
+
112
+ # 补全附件下载 URL(相对路径补上 BASE_URL)
113
+ def attachment_url(value)
114
+ return "" unless value.is_a?(String) && !value.empty?
115
+ return value if value.start_with?("http://", "https://")
116
+
117
+ "#{BASE_URL}#{value}"
118
+ end
119
+
120
+ # 将时间值统一为 ISO 8601(毫秒/秒时间戳或字符串)
121
+ def to_iso_time(value)
122
+ return "" if value.nil? || value == ""
123
+
124
+ if value.is_a?(Numeric)
125
+ millis = value > 1e12 ? value : value * 1000
126
+ return Time.at(millis / 1000.0).utc.iso8601
127
+ end
128
+
129
+ text = value.to_s
130
+ begin
131
+ Time.iso8601(text.sub(/Z\z/, "+00:00")).utc.iso8601
132
+ rescue ArgumentError
133
+ text
134
+ end
135
+ end
136
+
137
+ # 将邮件摘要与正文/附件展平为 normalize 可识别的字段
138
+ def flatten_message(summary, recipient_email, text_payload, html_payload, att_payload)
139
+ {
140
+ "id" => (summary["id"] || summary["messageId"] || "").to_s,
141
+ "from" => first_non_empty(summary["from"], summary["origfrom"]),
142
+ "to" => summary["to"] || recipient_email,
143
+ "subject" => summary["subject"] || "",
144
+ "text" => text_from_payload(text_payload, "text/plain"),
145
+ "html" => text_from_payload(html_payload, "text/html"),
146
+ "date" => to_iso_time(summary["time"] || summary["date"]),
147
+ "seen" => false,
148
+ "attachments" => build_attachments(att_payload)
149
+ }
150
+ end
151
+
152
+ # 展平附件列表
153
+ def build_attachments(att_payload)
154
+ list = att_payload.is_a?(Hash) ? att_payload["attachments"] : nil
155
+ return [] unless list.is_a?(Array)
156
+
157
+ list.filter_map do |att|
158
+ next unless att.is_a?(Hash)
159
+
160
+ {
161
+ "filename" => att["name"] || att["filename"] || "",
162
+ "size" => att["size"] || att["filesize"],
163
+ "contentType" => att["contentType"] || att["content_type"] ||
164
+ att["mimeType"] || att["mime_type"],
165
+ "url" => attachment_url(att["downloadUrl"] || att["url"])
166
+ }
167
+ end
168
+ end
169
+
170
+ # 返回首个非空字符串
171
+ def first_non_empty(*values)
172
+ values.each do |v|
173
+ next if v.nil?
174
+
175
+ t = v.to_s
176
+ return t unless t.empty?
177
+ end
178
+ ""
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # mailmomy 渠道实现 -- https://mailmomy.com
8
+ #
9
+ # 免注册、无鉴权、无 CAPTCHA 的纯 GET JSON API 临时邮箱,Token 即邮箱地址本身。
10
+ # 流程:
11
+ # 1. GET /api/domains/active -> JSON 字符串数组,随机选一个域名(失败回退 mailmomy.com)
12
+ # 2. 本地随机 10 位 [a-z0-9] local part,拼接为 <local>@<域名>
13
+ # 3. GET /api/mail/messages?to=<email>&page=1&limit=20 -> {"emails": [...], ...}
14
+ #
15
+ # 16 个域名变体渠道(如 16888888.cyou、sbook.pics 等)复用同一后端,
16
+ # 仅在 generate_email(domain) 传入固定域名即可,收信逻辑完全一致。
17
+ module Mailmomy
18
+ CHANNEL = "mailmomy"
19
+ BASE_URL = "https://mailmomy.com"
20
+ DEFAULT_DOMAIN = "mailmomy.com"
21
+ DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
22
+ "(KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
23
+
24
+ module_function
25
+
26
+ # 获取 User-Agent,优先使用配置中的自定义 UA
27
+ def user_agent
28
+ (Config.get_config.headers || {})["User-Agent"] || DEFAULT_UA
29
+ end
30
+
31
+ # 生成 n 位小写字母数字随机邮箱前缀
32
+ def random_local(length = 10)
33
+ chars = ("a".."z").to_a + ("0".."9").to_a
34
+ Array.new(length) { chars.sample }.join
35
+ end
36
+
37
+ # 按优先级从消息 Hash 中提取首个非空值并转为字符串
38
+ def first_value(msg, *keys)
39
+ keys.each do |key|
40
+ val = msg[key]
41
+ return val.to_s unless val.nil? || val == ""
42
+ end
43
+ ""
44
+ end
45
+
46
+ # 拉取 mailmomy 当前可用域名池并随机选取一个
47
+ # GET /api/domains/active -> JSON 字符串数组;请求或解析失败时回退到默认 mailmomy.com
48
+ def pick_domain
49
+ resp = Http.get("#{BASE_URL}/api/domains/active",
50
+ headers: { "User-Agent" => user_agent, "Accept" => "application/json" })
51
+ resp.raise_for_status
52
+ data = resp.json
53
+ return DEFAULT_DOMAIN unless data.is_a?(Array)
54
+
55
+ domains = data.select { |d| d.is_a?(String) && !d.empty? }
56
+ domains.empty? ? DEFAULT_DOMAIN : domains.sample
57
+ rescue StandardError
58
+ DEFAULT_DOMAIN
59
+ end
60
+
61
+ # 创建临时邮箱;服务免注册,直接构造 <local>@<域名> 即可收信
62
+ # @param domain [String, nil] 指定固定域名(域名变体渠道用);为空则从可用域名池随机选取
63
+ # @return [EmailInfo]
64
+ def generate_email(domain = nil)
65
+ selected = domain.to_s.strip
66
+ selected = pick_domain if selected.empty?
67
+ email = "#{random_local(10)}@#{selected}"
68
+ EmailInfo.new(channel: CHANNEL, email: email, token: email)
69
+ end
70
+
71
+ # 获取邮件列表
72
+ # GET /api/mail/messages?to=<email>&page=1&limit=20
73
+ # 返回 {"emails": [{id,recipient,from,subject,message,bodyText,receivedAt}], ...}
74
+ # @param email [String]
75
+ # @return [Array<Email>]
76
+ def get_emails(email)
77
+ addr = email.to_s.strip
78
+ raise "mailmomy: 缺少邮箱地址" if addr.empty?
79
+
80
+ url = "#{BASE_URL}/api/mail/messages?to=#{URI.encode_www_form_component(addr)}&page=1&limit=20"
81
+ resp = Http.get(url, headers: { "User-Agent" => user_agent, "Accept" => "application/json" })
82
+ resp.raise_for_status
83
+
84
+ data = resp.json
85
+ return [] unless data.is_a?(Hash)
86
+
87
+ emails = data["emails"]
88
+ return [] unless emails.is_a?(Array)
89
+
90
+ emails.filter_map do |msg|
91
+ next unless msg.is_a?(Hash)
92
+
93
+ message = first_value(msg, "message")
94
+ row = {
95
+ "id" => first_value(msg, "id"),
96
+ "from" => first_value(msg, "from"),
97
+ "to" => first_value(msg, "recipient").empty? ? addr : first_value(msg, "recipient"),
98
+ "subject" => first_value(msg, "subject"),
99
+ "text" => first_value(msg, "bodyText").empty? ? message : first_value(msg, "bodyText"),
100
+ "html" => message,
101
+ "date" => first_value(msg, "receivedAt")
102
+ }
103
+ Normalize.normalize_email(row, addr)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+ require "securerandom"
5
+ require "uri"
6
+
7
+ module TempmailSdk
8
+ module Providers
9
+ # Mailnesia 渠道 — https://mailnesia.com
10
+ # HTML 解析公开邮箱
11
+ module Mailnesia
12
+ CHANNEL = "mailnesia"
13
+ BASE_URL = "https://mailnesia.com"
14
+ DOMAIN = "mailnesia.com"
15
+ HEADERS = { "Accept" => "text/html,*/*" }.freeze
16
+
17
+ ROW_RE = /<tr\s+id="([^"]+)"[^>]*class="[^"]*\bemailheader\b[^"]*"[^>]*>(.*?)<\/tr>/mi
18
+ TIME_RE = /<time\s+datetime="([^"]+)"/i
19
+ ANCHOR_RE = /<a\b[^>]*class="email"[^>]*>(.*?)<\/a>/mi
20
+ TAG_RE = /<[^>]+>/
21
+
22
+ module_function
23
+
24
+ def random_local
25
+ chars = ("a".."z").to_a + ("0".."9").to_a
26
+ "sdk" + Array.new(16) { chars.sample }.join
27
+ end
28
+
29
+ def local_part(email)
30
+ (email || "").strip.split("@").first.to_s.strip
31
+ end
32
+
33
+ def mailbox_url(local)
34
+ "#{BASE_URL}/mailbox/#{URI.encode_www_form_component(local)}"
35
+ end
36
+
37
+ def detail_url(local, message_id)
38
+ "#{mailbox_url(local)}/#{URI.encode_www_form_component(message_id)}"
39
+ end
40
+
41
+ def fetch_html(url)
42
+ resp = Http.get(url, headers: HEADERS, timeout: 15)
43
+ resp.raise_for_status
44
+ resp.body
45
+ end
46
+
47
+ def clean_text(raw)
48
+ CGI.unescapeHTML(raw.to_s.gsub(TAG_RE, " ")).split.join(" ")
49
+ end
50
+
51
+ # 从邮箱列表页解析邮件条目
52
+ def parse_rows(page)
53
+ rows = []
54
+ page.scan(ROW_RE).each do |message_id, row_html|
55
+ message_id = message_id.strip
56
+ date_match = row_html.match(TIME_RE)
57
+ anchors = row_html.scan(ANCHOR_RE).map { |m| clean_text(m[0]) }
58
+ next if anchors.length < 3
59
+
60
+ rows << {
61
+ "id" => message_id,
62
+ "date" => date_match ? CGI.unescapeHTML(date_match[1].strip) : "",
63
+ "from" => anchors[0],
64
+ "to" => anchors[1],
65
+ "subject" => anchors[2]
66
+ }
67
+ end
68
+ rows
69
+ end
70
+
71
+ # 从详情页提取指定 div 内容
72
+ def extract_div_by_id(page, div_id, next_id = "")
73
+ needle = "id=\"#{div_id}\""
74
+ pos = page.index(needle)
75
+ return "" unless pos
76
+
77
+ open_end = page.index(">", pos)
78
+ return "" unless open_end
79
+
80
+ start = open_end + 1
81
+ end_pos = -1
82
+ end_pos = page.index("<div id=\"#{next_id}\"", start) unless next_id.empty?
83
+ if end_pos < 0
84
+ close = page.index("</div>", start)
85
+ end_pos = close + 6 if close
86
+ end
87
+ return "" if end_pos < 0
88
+
89
+ content = page[start...end_pos].strip
90
+ content = content[0...-6].strip if content.end_with?("</div>")
91
+ content
92
+ end
93
+
94
+ # 提取纯文本部分
95
+ def extract_plain(page, message_id)
96
+ pattern = /<div\s+id="text_plain_#{Regexp.escape(message_id)}"[^>]*>\s*<pre>(.*?)<\/pre>\s*<\/div>/mi
97
+ m = page.match(pattern)
98
+ m ? CGI.unescapeHTML(m[1]).strip : ""
99
+ end
100
+
101
+ # 获取单封详情
102
+ def fetch_detail(local, row)
103
+ message_id = (row["id"] || "").strip
104
+ return row if message_id.empty?
105
+
106
+ page = fetch_html(detail_url(local, message_id))
107
+ detail = row.dup
108
+ detail["text"] = extract_plain(page, message_id)
109
+ detail["html"] = extract_div_by_id(page, "text_html_#{message_id}", "text_plain_#{message_id}")
110
+ detail
111
+ end
112
+
113
+ # 创建临时邮箱
114
+ # @return [EmailInfo]
115
+ def generate_email
116
+ local = random_local
117
+ fetch_html(mailbox_url(local))
118
+ EmailInfo.new(channel: CHANNEL, email: "#{local}@#{DOMAIN}")
119
+ end
120
+
121
+ # 获取邮件列表
122
+ # @param email [String]
123
+ # @return [Array<Email>]
124
+ def get_emails(email)
125
+ local = local_part(email)
126
+ raise "mailnesia: empty email" if local.empty?
127
+
128
+ page = fetch_html(mailbox_url(local))
129
+ rows = parse_rows(page)
130
+
131
+ rows.map do |row|
132
+ begin
133
+ Normalize.normalize_email(fetch_detail(local, row), email)
134
+ rescue StandardError
135
+ Normalize.normalize_email(row, email)
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # MFFAC 渠道实现
8
+ # API: https://www.mffac.com/api
9
+ module Mffac
10
+ CHANNEL = "mffac"
11
+ BASE = "https://www.mffac.com/api"
12
+
13
+ HEADERS = {
14
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
15
+ "Content-Type" => "application/json",
16
+ "Accept" => "*/*",
17
+ "Origin" => "https://www.mffac.com",
18
+ "Referer" => "https://www.mffac.com/"
19
+ }.freeze
20
+
21
+ GET_HEADERS = HEADERS.reject { |k, _| k == "Content-Type" }.freeze
22
+
23
+ module_function
24
+
25
+ # 将 receivedAt 秒级时间戳转为 ISO8601
26
+ def received_at_to_iso(value)
27
+ seconds = Float(value)
28
+ return "" if seconds <= 0
29
+
30
+ Time.at(seconds).utc.strftime("%Y-%m-%dT%H:%M:%SZ")
31
+ rescue TypeError, ArgumentError
32
+ ""
33
+ end
34
+
35
+ # 扁平化邮件字段
36
+ def flatten_email(raw, recipient)
37
+ {
38
+ "id" => raw["id"] || "",
39
+ "from" => raw["fromAddress"] || "",
40
+ "to" => raw["toAddress"] || recipient,
41
+ "subject" => raw["subject"] || "",
42
+ "text" => raw["textContent"] || "",
43
+ "html" => raw["htmlContent"] || "",
44
+ "date" => received_at_to_iso(raw["receivedAt"]),
45
+ "isRead" => raw["isRead"] || false,
46
+ "attachments" => []
47
+ }
48
+ end
49
+
50
+ # 获取单封邮件详情
51
+ def fetch_detail(message_id)
52
+ r = Http.get("#{BASE}/emails/#{URI.encode_www_form_component(message_id)}", headers: GET_HEADERS)
53
+ return nil unless r.ok?
54
+
55
+ data = r.json
56
+ return nil unless data.is_a?(Hash) && data["success"]
57
+
58
+ email = data["email"]
59
+ email.is_a?(Hash) ? email : nil
60
+ rescue StandardError
61
+ nil
62
+ end
63
+
64
+ # 创建临时邮箱
65
+ # @return [EmailInfo]
66
+ def generate_email
67
+ r = Http.post("#{BASE}/mailboxes", headers: HEADERS, json: { "expiresInHours" => 24 })
68
+ r.raise_for_status
69
+ data = r.json
70
+ raise "mffac: 创建失败" unless data.is_a?(Hash) && data["success"] && data["mailbox"]
71
+
72
+ mb = data["mailbox"]
73
+ addr = (mb["address"] || "").strip
74
+ mid = (mb["id"] || "").strip
75
+ raise "mffac: 无效的邮箱数据" if addr.empty? || mid.empty?
76
+
77
+ EmailInfo.new(channel: CHANNEL, email: "#{addr}@mffac.com", token: mid, created_at: mb["createdAt"])
78
+ end
79
+
80
+ # 获取邮件列表
81
+ # @param email [String]
82
+ # @param token [String]
83
+ # @return [Array<Email>]
84
+ def get_emails(email, token = "")
85
+ local = email.to_s.split("@", 2).first.to_s.strip
86
+ raise "mffac: 地址为空" if local.empty?
87
+
88
+ r = Http.get("#{BASE}/mailboxes/#{local}/emails", headers: GET_HEADERS)
89
+ r.raise_for_status
90
+ data = r.json
91
+ raise "mffac: 获取列表失败" unless data.is_a?(Hash) && data["success"]
92
+
93
+ (data["emails"] || []).select { |raw| raw.is_a?(Hash) }.map do |raw|
94
+ mid = raw["id"].to_s.strip
95
+ detail = mid.empty? ? nil : fetch_detail(mid)
96
+ Normalize.normalize_email(flatten_email(detail || raw, email), email)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end