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.
- checksums.yaml +7 -0
- data/README.md +116 -0
- data/lib/tempmail_sdk/channels.rb +289 -0
- data/lib/tempmail_sdk/channels_data.rb +289 -0
- data/lib/tempmail_sdk/client.rb +193 -0
- data/lib/tempmail_sdk/config.rb +104 -0
- data/lib/tempmail_sdk/html_utils.rb +42 -0
- data/lib/tempmail_sdk/http.rb +225 -0
- data/lib/tempmail_sdk/logger.rb +38 -0
- data/lib/tempmail_sdk/normalize.rb +193 -0
- data/lib/tempmail_sdk/providers/anonbox.rb +248 -0
- data/lib/tempmail_sdk/providers/anonymmail.rb +92 -0
- data/lib/tempmail_sdk/providers/byom.rb +48 -0
- data/lib/tempmail_sdk/providers/catchmail.rb +110 -0
- data/lib/tempmail_sdk/providers/cleantempmail.rb +49 -0
- data/lib/tempmail_sdk/providers/devmail_uk.rb +58 -0
- data/lib/tempmail_sdk/providers/dropmail.rb +133 -0
- data/lib/tempmail_sdk/providers/dropmail_click.rb +81 -0
- data/lib/tempmail_sdk/providers/dropmail_me.rb +163 -0
- data/lib/tempmail_sdk/providers/duckmail.rb +63 -0
- data/lib/tempmail_sdk/providers/eyepaste.rb +119 -0
- data/lib/tempmail_sdk/providers/fake_email_site.rb +48 -0
- data/lib/tempmail_sdk/providers/fake_legal.rb +109 -0
- data/lib/tempmail_sdk/providers/fmail.rb +87 -0
- data/lib/tempmail_sdk/providers/freecustom.rb +135 -0
- data/lib/tempmail_sdk/providers/getnada.rb +146 -0
- data/lib/tempmail_sdk/providers/gonebox_email.rb +89 -0
- data/lib/tempmail_sdk/providers/guerrillamail.rb +62 -0
- data/lib/tempmail_sdk/providers/guerrillamail_mirrors.rb +81 -0
- data/lib/tempmail_sdk/providers/harakirimail.rb +84 -0
- data/lib/tempmail_sdk/providers/haribu.rb +176 -0
- data/lib/tempmail_sdk/providers/inboxes.rb +110 -0
- data/lib/tempmail_sdk/providers/inboxkitten.rb +90 -0
- data/lib/tempmail_sdk/providers/linshiyou.rb +157 -0
- data/lib/tempmail_sdk/providers/lroid.rb +170 -0
- data/lib/tempmail_sdk/providers/m2u.rb +126 -0
- data/lib/tempmail_sdk/providers/mail10s.rb +59 -0
- data/lib/tempmail_sdk/providers/mail123.rb +82 -0
- data/lib/tempmail_sdk/providers/mail_cx.rb +133 -0
- data/lib/tempmail_sdk/providers/mail_sunls.rb +68 -0
- data/lib/tempmail_sdk/providers/mail_tm.rb +99 -0
- data/lib/tempmail_sdk/providers/mailcatch.rb +91 -0
- data/lib/tempmail_sdk/providers/maildrop.rb +92 -0
- data/lib/tempmail_sdk/providers/maildrop_cc.rb +110 -0
- data/lib/tempmail_sdk/providers/mailforspam.rb +96 -0
- data/lib/tempmail_sdk/providers/mailhole_de.rb +70 -0
- data/lib/tempmail_sdk/providers/mailinator.rb +182 -0
- data/lib/tempmail_sdk/providers/mailmomy.rb +108 -0
- data/lib/tempmail_sdk/providers/mailnesia.rb +141 -0
- data/lib/tempmail_sdk/providers/mffac.rb +101 -0
- data/lib/tempmail_sdk/providers/moakt.rb +296 -0
- data/lib/tempmail_sdk/providers/neighbours.rb +140 -0
- data/lib/tempmail_sdk/providers/neighbours_sh.rb +117 -0
- data/lib/tempmail_sdk/providers/nimail.rb +100 -0
- data/lib/tempmail_sdk/providers/ockito.rb +163 -0
- data/lib/tempmail_sdk/providers/one_sec_mail.rb +46 -0
- data/lib/tempmail_sdk/providers/restmail_net.rb +60 -0
- data/lib/tempmail_sdk/providers/rootsh.rb +167 -0
- data/lib/tempmail_sdk/providers/shitty_email.rb +84 -0
- data/lib/tempmail_sdk/providers/smailpro.rb +103 -0
- data/lib/tempmail_sdk/providers/ta_easy.rb +53 -0
- data/lib/tempmail_sdk/providers/temp_mail_io.rb +67 -0
- data/lib/tempmail_sdk/providers/temp_mail_now.rb +103 -0
- data/lib/tempmail_sdk/providers/temp_mail_org.rb +89 -0
- data/lib/tempmail_sdk/providers/tempemail_co.rb +142 -0
- data/lib/tempmail_sdk/providers/tempemail_info.rb +121 -0
- data/lib/tempmail_sdk/providers/tempemails_net.rb +159 -0
- data/lib/tempmail_sdk/providers/tempfastmail.rb +76 -0
- data/lib/tempmail_sdk/providers/tempgmailer.rb +126 -0
- data/lib/tempmail_sdk/providers/tempgo_email.rb +74 -0
- data/lib/tempmail_sdk/providers/tempinbox.rb +70 -0
- data/lib/tempmail_sdk/providers/tempmail.rb +66 -0
- data/lib/tempmail_sdk/providers/tempmail365.rb +119 -0
- data/lib/tempmail_sdk/providers/tempmail_fish.rb +89 -0
- data/lib/tempmail_sdk/providers/tempmail_fyi.rb +124 -0
- data/lib/tempmail_sdk/providers/tempmail_lol.rb +42 -0
- data/lib/tempmail_sdk/providers/tempmail_lol_v2.rb +57 -0
- data/lib/tempmail_sdk/providers/tempmail_plus.rb +80 -0
- data/lib/tempmail_sdk/providers/tempmailc.rb +80 -0
- data/lib/tempmail_sdk/providers/tempmailpro.rb +89 -0
- data/lib/tempmail_sdk/providers/tempmailten.rb +150 -0
- data/lib/tempmail_sdk/providers/temporam.rb +95 -0
- data/lib/tempmail_sdk/providers/tempp_mails.rb +150 -0
- data/lib/tempmail_sdk/providers/tempy_email.rb +73 -0
- data/lib/tempmail_sdk/providers/ten_minute_mail_net.rb +125 -0
- data/lib/tempmail_sdk/providers/tenminute_one.rb +255 -0
- data/lib/tempmail_sdk/providers/throwawaymail.rb +80 -0
- data/lib/tempmail_sdk/providers/tmail_link.rb +98 -0
- data/lib/tempmail_sdk/providers/uncorreotemporal.rb +84 -0
- data/lib/tempmail_sdk/providers/webmailtemp.rb +108 -0
- data/lib/tempmail_sdk/providers/xkx_me.rb +125 -0
- data/lib/tempmail_sdk/providers/zhujump.rb +256 -0
- data/lib/tempmail_sdk/registry.rb +1097 -0
- data/lib/tempmail_sdk/retry.rb +91 -0
- data/lib/tempmail_sdk/telemetry.rb +134 -0
- data/lib/tempmail_sdk/types.rb +164 -0
- data/lib/tempmail_sdk/version.rb +6 -0
- data/lib/tempmail_sdk/webui.rb +360 -0
- data/lib/tempmail_sdk.rb +76 -0
- 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
|