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,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# Lroid 渠道 — https://lroid.com
|
|
8
|
+
# HTML 解析模式,session cookies 维持邮箱身份
|
|
9
|
+
module Lroid
|
|
10
|
+
CHANNEL = "lroid"
|
|
11
|
+
BASE_URL = "https://lroid.com"
|
|
12
|
+
|
|
13
|
+
EMAIL_RE = /<input[^>]+id=["']eposta_adres["'][^>]+value=["']([^"']+@[^"']+)["']/i
|
|
14
|
+
EMAIL_RE_ALT = /<input[^>]+value=["']([^"']+@[^"']+)["'][^>]+id=["']eposta_adres["']/i
|
|
15
|
+
|
|
16
|
+
HEADERS = {
|
|
17
|
+
"Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
18
|
+
"Accept-Language" => "en-US,en;q=0.9",
|
|
19
|
+
"Referer" => "#{BASE_URL}/",
|
|
20
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
21
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# 从响应中收集并序列化 cookies
|
|
27
|
+
def cookie_header_from_resp(resp)
|
|
28
|
+
map = {}
|
|
29
|
+
resp.set_cookies.each do |line|
|
|
30
|
+
pair = line.split(";", 2).first.to_s.strip
|
|
31
|
+
next unless pair.include?("=")
|
|
32
|
+
|
|
33
|
+
k, v = pair.split("=", 2)
|
|
34
|
+
map[k.strip] = v.to_s.strip unless k.strip.empty?
|
|
35
|
+
end
|
|
36
|
+
map.keys.sort.map { |k| "#{k}=#{map[k]}" }.join("; ")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# 从 HTML 中提取邮箱地址
|
|
40
|
+
def extract_email(html_str)
|
|
41
|
+
m = html_str.match(EMAIL_RE) || html_str.match(EMAIL_RE_ALT)
|
|
42
|
+
raise "lroid: 无法从 HTML 响应中解析邮箱地址" unless m
|
|
43
|
+
|
|
44
|
+
addr = m[1].strip
|
|
45
|
+
raise "lroid: 解析到的邮箱地址无效" unless addr.include?("@")
|
|
46
|
+
|
|
47
|
+
addr
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# 解码 token
|
|
51
|
+
def decode_token(token)
|
|
52
|
+
data = JSON.parse(token)
|
|
53
|
+
cookie = (data["cookie"] || "").strip
|
|
54
|
+
raise "lroid: session token 中缺少 cookie" if cookie.empty?
|
|
55
|
+
|
|
56
|
+
cookie
|
|
57
|
+
rescue JSON::ParserError
|
|
58
|
+
raise "lroid: 无效的 session token"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# 创建临时邮箱
|
|
62
|
+
# @return [EmailInfo]
|
|
63
|
+
def generate_email
|
|
64
|
+
resp = Http.get(BASE_URL, headers: HEADERS, timeout: 15)
|
|
65
|
+
resp.raise_for_status
|
|
66
|
+
email = extract_email(resp.body)
|
|
67
|
+
cookie_hdr = cookie_header_from_resp(resp)
|
|
68
|
+
token = JSON.generate("cookie" => cookie_hdr)
|
|
69
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: token)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# 获取邮件列表
|
|
73
|
+
# @param token [String]
|
|
74
|
+
# @param email [String]
|
|
75
|
+
# @return [Array<Email>]
|
|
76
|
+
def get_emails(token, email = "")
|
|
77
|
+
cookie = decode_token(token)
|
|
78
|
+
address = (email || "").strip
|
|
79
|
+
raise "lroid: 邮箱地址不能为空" if address.empty?
|
|
80
|
+
|
|
81
|
+
# 尝试 kontrol API
|
|
82
|
+
begin
|
|
83
|
+
resp = Http.get(
|
|
84
|
+
"#{BASE_URL}/en/api-kontrol/",
|
|
85
|
+
headers: {
|
|
86
|
+
"Accept" => "application/json, text/html, */*;q=0.8",
|
|
87
|
+
"Referer" => "#{BASE_URL}/",
|
|
88
|
+
"Cookie" => cookie,
|
|
89
|
+
"User-Agent" => HEADERS["User-Agent"]
|
|
90
|
+
},
|
|
91
|
+
timeout: 15
|
|
92
|
+
)
|
|
93
|
+
resp.raise_for_status
|
|
94
|
+
data = resp.json
|
|
95
|
+
if data.is_a?(Array)
|
|
96
|
+
return parse_json_emails(data, address)
|
|
97
|
+
elsif data.is_a?(Hash)
|
|
98
|
+
%w[mails emails messages data inbox].each do |key|
|
|
99
|
+
items = data[key]
|
|
100
|
+
return parse_json_emails(items, address) if items.is_a?(Array)
|
|
101
|
+
end
|
|
102
|
+
if data["id"] || data["subject"] || data["from"]
|
|
103
|
+
return parse_json_emails([data], address)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
rescue StandardError
|
|
107
|
+
# API 不可用,回退到 HTML 解析
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
parse_html_emails(cookie, address)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def parse_json_emails(items, recipient)
|
|
114
|
+
items.filter_map do |raw|
|
|
115
|
+
next unless raw.is_a?(Hash)
|
|
116
|
+
|
|
117
|
+
normalized = raw.dup
|
|
118
|
+
normalized["html"] = raw["body"] || raw["content"] if !raw.key?("html")
|
|
119
|
+
Normalize.normalize_email(normalized, recipient)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def parse_html_emails(cookie, recipient)
|
|
124
|
+
resp = Http.get(
|
|
125
|
+
BASE_URL,
|
|
126
|
+
headers: {
|
|
127
|
+
"Accept" => "text/html,application/xhtml+xml,*/*;q=0.8",
|
|
128
|
+
"Referer" => "#{BASE_URL}/",
|
|
129
|
+
"Cookie" => cookie,
|
|
130
|
+
"User-Agent" => HEADERS["User-Agent"]
|
|
131
|
+
},
|
|
132
|
+
timeout: 15
|
|
133
|
+
)
|
|
134
|
+
resp.raise_for_status
|
|
135
|
+
html_str = resp.body
|
|
136
|
+
|
|
137
|
+
# 解析 <li class="mail"> 元素
|
|
138
|
+
mail_items = html_str.scan(/<li[^>]*class=["'][^"']*\bmail\b[^"']*["'][^>]*>(.*?)<\/li>/mi)
|
|
139
|
+
return [] if mail_items.empty?
|
|
140
|
+
|
|
141
|
+
mail_items.each_with_index.filter_map do |(item_html), idx|
|
|
142
|
+
subject = ""
|
|
143
|
+
from_addr = ""
|
|
144
|
+
date = ""
|
|
145
|
+
mail_id = (idx + 1).to_s
|
|
146
|
+
|
|
147
|
+
# 提取主题
|
|
148
|
+
sm = item_html.match(/class=["'][^"']*\bsubject\b[^"']*["'][^>]*>(.*?)<\//mi)
|
|
149
|
+
subject = sm[1].gsub(/<[^>]+>/, "").strip if sm
|
|
150
|
+
|
|
151
|
+
# 提取发件人
|
|
152
|
+
fm = item_html.match(/class=["'][^"']*\b(?:from|sender)\b[^"']*["'][^>]*>(.*?)<\//mi)
|
|
153
|
+
from_addr = fm[1].gsub(/<[^>]+>/, "").strip if fm
|
|
154
|
+
|
|
155
|
+
# 提取日期
|
|
156
|
+
dm = item_html.match(/class=["'][^"']*\b(?:date|time)\b[^"']*["'][^>]*>(.*?)<\//mi)
|
|
157
|
+
date = dm[1].gsub(/<[^>]+>/, "").strip if dm
|
|
158
|
+
|
|
159
|
+
Normalize.normalize_email({
|
|
160
|
+
"id" => mail_id,
|
|
161
|
+
"from" => from_addr,
|
|
162
|
+
"to" => recipient,
|
|
163
|
+
"subject" => subject,
|
|
164
|
+
"date" => date
|
|
165
|
+
}, recipient)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# m2u.io 渠道实现
|
|
8
|
+
module M2u
|
|
9
|
+
CHANNEL = "m2u"
|
|
10
|
+
API_BASE = "https://api.m2u.io"
|
|
11
|
+
HEADERS = {
|
|
12
|
+
"Accept" => "application/json",
|
|
13
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
14
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def pack_token(token, view_token)
|
|
20
|
+
JSON.generate({ "token" => token, "viewToken" => view_token })
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def unpack_token(token)
|
|
24
|
+
value = token.to_s.strip
|
|
25
|
+
return ["", ""] if value.empty?
|
|
26
|
+
|
|
27
|
+
if value.start_with?("{")
|
|
28
|
+
begin
|
|
29
|
+
data = JSON.parse(value)
|
|
30
|
+
if data.is_a?(Hash)
|
|
31
|
+
return [data["token"].to_s.strip, data["viewToken"].to_s.strip]
|
|
32
|
+
end
|
|
33
|
+
rescue StandardError
|
|
34
|
+
return ["", ""]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
[value, ""]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def flatten_message(raw, recipient)
|
|
41
|
+
{
|
|
42
|
+
"id" => (raw["id"] || raw["message_id"]).to_s,
|
|
43
|
+
"from" => (raw["from_addr"] || raw["from_address"] || raw["from"]).to_s,
|
|
44
|
+
"to" => recipient,
|
|
45
|
+
"subject" => (raw["subject"] || "").to_s,
|
|
46
|
+
"text" => (raw["text_body"] || raw["body_text"] || raw["text"]).to_s,
|
|
47
|
+
"html" => (raw["html_body"] || raw["body_html"] || raw["html"]).to_s,
|
|
48
|
+
"date" => (raw["received_at"] || raw["created_at"]).to_s,
|
|
49
|
+
"is_read" => raw["is_read"] || raw["isRead"] || raw["seen"] || false,
|
|
50
|
+
"attachments" => raw["attachments"].is_a?(Array) ? raw["attachments"] : []
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [EmailInfo]
|
|
55
|
+
def generate_email
|
|
56
|
+
resp = Http.post(
|
|
57
|
+
"#{API_BASE}/v1/mailboxes/auto",
|
|
58
|
+
headers: HEADERS.merge("Content-Type" => "application/json"),
|
|
59
|
+
json: {},
|
|
60
|
+
timeout: 15
|
|
61
|
+
)
|
|
62
|
+
resp.raise_for_status
|
|
63
|
+
data = resp.json
|
|
64
|
+
mailbox = data.is_a?(Hash) ? data["mailbox"] : nil
|
|
65
|
+
raise "m2u: invalid mailbox response" unless mailbox.is_a?(Hash)
|
|
66
|
+
|
|
67
|
+
local_part = mailbox["local_part"].to_s.strip
|
|
68
|
+
domain = mailbox["domain"].to_s.strip
|
|
69
|
+
token = mailbox["token"].to_s.strip
|
|
70
|
+
view_token = mailbox["view_token"].to_s.strip
|
|
71
|
+
email = !local_part.empty? && !domain.empty? ? "#{local_part}@#{domain}" : ""
|
|
72
|
+
raise "m2u: invalid mailbox response" if email.empty? || token.empty? || view_token.empty?
|
|
73
|
+
|
|
74
|
+
EmailInfo.new(
|
|
75
|
+
channel: CHANNEL,
|
|
76
|
+
email: email,
|
|
77
|
+
token: pack_token(token, view_token),
|
|
78
|
+
expires_at: mailbox["expires_at"],
|
|
79
|
+
created_at: mailbox["created_at"]
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def fetch_detail(token, view_token, message_id)
|
|
84
|
+
resp = Http.get(
|
|
85
|
+
"#{API_BASE}/v1/mailboxes/#{URI.encode_www_form_component(token)}/messages/#{URI.encode_www_form_component(message_id)}?view=#{URI.encode_www_form_component(view_token)}",
|
|
86
|
+
headers: HEADERS, timeout: 15
|
|
87
|
+
)
|
|
88
|
+
return nil unless resp.ok?
|
|
89
|
+
|
|
90
|
+
data = resp.json
|
|
91
|
+
return nil unless data.is_a?(Hash)
|
|
92
|
+
|
|
93
|
+
detail = data["message"]
|
|
94
|
+
detail.is_a?(Hash) ? detail : nil
|
|
95
|
+
rescue StandardError
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @param token [String] JSON 打包的 token
|
|
100
|
+
# @param email [String]
|
|
101
|
+
# @return [Array<Email>]
|
|
102
|
+
def get_emails(token, email)
|
|
103
|
+
mailbox_token, view_token = unpack_token(token)
|
|
104
|
+
address = email.to_s.strip
|
|
105
|
+
raise "m2u: missing token" if mailbox_token.empty?
|
|
106
|
+
raise "m2u: missing view token" if view_token.empty?
|
|
107
|
+
raise "m2u: empty email" if address.empty?
|
|
108
|
+
|
|
109
|
+
resp = Http.get(
|
|
110
|
+
"#{API_BASE}/v1/mailboxes/#{URI.encode_www_form_component(mailbox_token)}/messages?view=#{URI.encode_www_form_component(view_token)}",
|
|
111
|
+
headers: HEADERS, timeout: 15
|
|
112
|
+
)
|
|
113
|
+
resp.raise_for_status
|
|
114
|
+
data = resp.json
|
|
115
|
+
rows = data.is_a?(Hash) ? (data["messages"] || []) : []
|
|
116
|
+
return [] unless rows.is_a?(Array)
|
|
117
|
+
|
|
118
|
+
rows.select { |r| r.is_a?(Hash) }.map do |item|
|
|
119
|
+
message_id = (item["id"] || item["message_id"]).to_s.strip
|
|
120
|
+
detail = message_id.empty? ? nil : fetch_detail(mailbox_token, view_token, message_id)
|
|
121
|
+
Normalize.normalize_email(flatten_message(detail || item, address), address)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# mail10s.com 渠道实现
|
|
6
|
+
module Mail10s
|
|
7
|
+
CHANNEL = "mail10s"
|
|
8
|
+
BASE_URL = "https://mail10s.com"
|
|
9
|
+
DOMAIN = "mail10s.com"
|
|
10
|
+
HEADERS = { "Accept" => "application/json", "User-Agent" => "Mozilla/5.0" }.freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def random_local
|
|
15
|
+
chars = ("a".."z").to_a + ("0".."9").to_a
|
|
16
|
+
"sdk" + Array.new(16) { chars.sample }.join
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def flatten(raw, recipient)
|
|
20
|
+
{
|
|
21
|
+
"id" => raw["id"].to_s,
|
|
22
|
+
"from" => (raw["sender"] || "").to_s,
|
|
23
|
+
"to" => recipient,
|
|
24
|
+
"subject" => (raw["subject"] || "").to_s,
|
|
25
|
+
"text" => (raw["body_text"] || "").to_s,
|
|
26
|
+
"html" => (raw["body_html"] || "").to_s,
|
|
27
|
+
"date" => (raw["received_at"] || "").to_s,
|
|
28
|
+
"attachments" => raw["attachments"].is_a?(Array) ? raw["attachments"] : []
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [EmailInfo]
|
|
33
|
+
def generate_email
|
|
34
|
+
email = "#{random_local}@#{DOMAIN}"
|
|
35
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: email)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param email [String]
|
|
39
|
+
# @return [Array<Email>]
|
|
40
|
+
def get_emails(email)
|
|
41
|
+
address = email.to_s.strip
|
|
42
|
+
raise "mail10s: empty email" if address.empty?
|
|
43
|
+
|
|
44
|
+
resp = Http.get(
|
|
45
|
+
"#{BASE_URL}/api/emails/#{URI.encode_www_form_component(address)}/inbox",
|
|
46
|
+
headers: HEADERS, timeout: 15
|
|
47
|
+
)
|
|
48
|
+
resp.raise_for_status
|
|
49
|
+
data = resp.json
|
|
50
|
+
messages = if data.is_a?(Hash) && data["data"].is_a?(Hash)
|
|
51
|
+
data["data"]["messages"]
|
|
52
|
+
end
|
|
53
|
+
return [] unless messages.is_a?(Array)
|
|
54
|
+
|
|
55
|
+
messages.select { |r| r.is_a?(Hash) }.map { |row| Normalize.normalize_email(flatten(row, address), address) }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# mail123.fr 渠道实现
|
|
6
|
+
module Mail123
|
|
7
|
+
CHANNEL = "mail123"
|
|
8
|
+
API_BASE = "https://mail123.fr/api/v1"
|
|
9
|
+
HEADERS = { "Accept" => "application/json", "User-Agent" => "Mozilla/5.0" }.freeze
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def flatten(raw, recipient)
|
|
14
|
+
out = raw.dup
|
|
15
|
+
out["id"] = raw["id"].to_s
|
|
16
|
+
out["to"] = (raw["to"] || recipient).to_s
|
|
17
|
+
out["text"] = (raw["text"] || raw["preview"] || "").to_s
|
|
18
|
+
out["html"] = (raw["html"] || "").to_s
|
|
19
|
+
if raw.key?("is_unread") && (raw["is_unread"] == true || raw["is_unread"] == false)
|
|
20
|
+
out["isRead"] = !raw["is_unread"]
|
|
21
|
+
end
|
|
22
|
+
out
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [EmailInfo]
|
|
26
|
+
def generate_email
|
|
27
|
+
resp = Http.get("#{API_BASE}/mailbox/new", headers: HEADERS, timeout: 15)
|
|
28
|
+
resp.raise_for_status
|
|
29
|
+
data = resp.json
|
|
30
|
+
raise "mail123: invalid mailbox response" unless data.is_a?(Hash)
|
|
31
|
+
|
|
32
|
+
email = data["address"].to_s.strip
|
|
33
|
+
raise "mail123: invalid mailbox response" if email.empty? || !email.include?("@")
|
|
34
|
+
|
|
35
|
+
expires_at = nil
|
|
36
|
+
if data["expires_in_days"].is_a?(Numeric) && data["expires_in_days"] > 0
|
|
37
|
+
expires_at = ((Time.now.to_f + data["expires_in_days"].to_f * 86_400) * 1000).to_i
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: email, expires_at: expires_at)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def fetch_detail(address, message_id)
|
|
44
|
+
resp = Http.get(
|
|
45
|
+
"#{API_BASE}/mailbox/#{URI.encode_www_form_component(address)}/messages/#{URI.encode_www_form_component(message_id)}",
|
|
46
|
+
headers: HEADERS, timeout: 15
|
|
47
|
+
)
|
|
48
|
+
return nil unless resp.ok?
|
|
49
|
+
|
|
50
|
+
data = resp.json
|
|
51
|
+
if data.is_a?(Hash) && data["message"].is_a?(Hash)
|
|
52
|
+
return data["message"]
|
|
53
|
+
end
|
|
54
|
+
nil
|
|
55
|
+
rescue StandardError
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @param email [String]
|
|
60
|
+
# @return [Array<Email>]
|
|
61
|
+
def get_emails(email)
|
|
62
|
+
address = email.to_s.strip
|
|
63
|
+
raise "mail123: empty email" if address.empty?
|
|
64
|
+
|
|
65
|
+
resp = Http.get(
|
|
66
|
+
"#{API_BASE}/mailbox/#{URI.encode_www_form_component(address)}/messages?limit=50",
|
|
67
|
+
headers: HEADERS, timeout: 15
|
|
68
|
+
)
|
|
69
|
+
resp.raise_for_status
|
|
70
|
+
data = resp.json
|
|
71
|
+
rows = data.is_a?(Hash) ? (data["messages"] || []) : []
|
|
72
|
+
return [] unless rows.is_a?(Array)
|
|
73
|
+
|
|
74
|
+
rows.select { |r| r.is_a?(Hash) }.map do |item|
|
|
75
|
+
message_id = (item["id"] || "").to_s.strip
|
|
76
|
+
detail = message_id.empty? ? nil : fetch_detail(address, message_id)
|
|
77
|
+
Normalize.normalize_email(flatten(detail || item, address), address)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module TempmailSdk
|
|
7
|
+
module Providers
|
|
8
|
+
# mail.cx 渠道实现
|
|
9
|
+
# 匿名 Web API: https://mail.cx/v1
|
|
10
|
+
module MailCx
|
|
11
|
+
CHANNEL = "mail-cx"
|
|
12
|
+
BASE_URL = "https://mail.cx"
|
|
13
|
+
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def random_string(length)
|
|
18
|
+
chars = ("a".."z").to_a + ("0".."9").to_a
|
|
19
|
+
Array.new(length) { chars.sample }.join
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def client_id
|
|
23
|
+
"tempmail-sdk-#{random_string(16)}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def headers(cid)
|
|
27
|
+
{
|
|
28
|
+
"Accept" => "application/json", "Origin" => BASE_URL, "Referer" => "#{BASE_URL}/",
|
|
29
|
+
"User-Agent" => UA, "X-Client-ID" => cid
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def fetch_config(cid)
|
|
34
|
+
resp = Http.get("#{BASE_URL}/v1/config", headers: headers(cid), timeout: 15)
|
|
35
|
+
resp.raise_for_status
|
|
36
|
+
data = resp.json
|
|
37
|
+
data.is_a?(Hash) ? data : {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def pick_domain(config, preferred)
|
|
41
|
+
items = config["system_domains"].is_a?(Array) ? config["system_domains"] : []
|
|
42
|
+
domains = items.select { |i| i.is_a?(Hash) && !i["domain"].to_s.strip.empty? }
|
|
43
|
+
.map { |i| i["domain"].to_s.strip }
|
|
44
|
+
raise "mail-cx: no system domains" if domains.empty?
|
|
45
|
+
|
|
46
|
+
pref = preferred.to_s.strip.sub(/\A@/, "").downcase
|
|
47
|
+
unless pref.empty?
|
|
48
|
+
found = domains.find { |d| d.downcase == pref }
|
|
49
|
+
return found if found
|
|
50
|
+
end
|
|
51
|
+
default = items.find { |i| i.is_a?(Hash) && i["default"] && !i["domain"].to_s.strip.empty? }
|
|
52
|
+
default ? default["domain"].to_s.strip : domains.first
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def first_non_empty(*values)
|
|
56
|
+
values.each do |v|
|
|
57
|
+
next if v.nil?
|
|
58
|
+
|
|
59
|
+
t = v.to_s.strip
|
|
60
|
+
return t unless t.empty?
|
|
61
|
+
end
|
|
62
|
+
""
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @param preferred_domain [String, nil]
|
|
66
|
+
# @return [EmailInfo]
|
|
67
|
+
def generate_email(preferred_domain = nil)
|
|
68
|
+
cid = client_id
|
|
69
|
+
config = fetch_config(cid)
|
|
70
|
+
domain = pick_domain(config, preferred_domain)
|
|
71
|
+
created = Time.now.utc
|
|
72
|
+
expires_at = nil
|
|
73
|
+
ttl = config["ttl_seconds"]
|
|
74
|
+
expires_at = ((created + ttl).to_f * 1000).to_i if ttl.is_a?(Numeric) && ttl.positive?
|
|
75
|
+
EmailInfo.new(channel: CHANNEL, email: "#{random_string(12)}@#{domain}",
|
|
76
|
+
token: cid, expires_at: expires_at, created_at: created.iso8601)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def fetch_detail(cid, message_id)
|
|
80
|
+
resp = Http.get("#{BASE_URL}/v1/email/#{URI.encode_www_form_component(message_id)}",
|
|
81
|
+
headers: headers(cid), timeout: 15)
|
|
82
|
+
return nil unless resp.ok?
|
|
83
|
+
|
|
84
|
+
data = resp.json
|
|
85
|
+
data.is_a?(Hash) ? data : nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @param email [String]
|
|
89
|
+
# @param client_id [String]
|
|
90
|
+
# @return [Array<Email>]
|
|
91
|
+
def get_emails(email, client_id)
|
|
92
|
+
resp = Http.get("#{BASE_URL}/v1/inbox/#{URI.encode_www_form_component(email)}",
|
|
93
|
+
headers: headers(client_id), timeout: 30)
|
|
94
|
+
return [] if resp.status_code == 204
|
|
95
|
+
|
|
96
|
+
resp.raise_for_status
|
|
97
|
+
data = resp.json
|
|
98
|
+
rows = data.is_a?(Hash) ? data["emails"] : nil
|
|
99
|
+
return [] unless rows.is_a?(Array)
|
|
100
|
+
|
|
101
|
+
rows.map do |row|
|
|
102
|
+
unless row.is_a?(Hash)
|
|
103
|
+
next Normalize.normalize_email({}, email)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
raw = {
|
|
107
|
+
"id" => row["id"], "from" => first_non_empty(row["from_email"], row["from_name"]),
|
|
108
|
+
"to" => email, "subject" => row["subject"] || "", "text" => row["preview_text"] || "",
|
|
109
|
+
"created_at" => row["created_at"], "attachments" => row["attachments"]
|
|
110
|
+
}
|
|
111
|
+
mid = row["id"].to_s.strip
|
|
112
|
+
unless mid.empty?
|
|
113
|
+
begin
|
|
114
|
+
detail = fetch_detail(client_id, mid)
|
|
115
|
+
if detail
|
|
116
|
+
raw = {
|
|
117
|
+
"id" => detail["id"], "from" => first_non_empty(detail["from_email"], detail["from_name"]),
|
|
118
|
+
"to" => email, "subject" => detail["subject"] || "",
|
|
119
|
+
"text" => first_non_empty(detail["text_body"], detail["preview_text"]),
|
|
120
|
+
"html" => detail["html_body"] || "", "created_at" => detail["created_at"],
|
|
121
|
+
"attachments" => detail["attachments"]
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
rescue StandardError
|
|
125
|
+
# 忽略详情失败
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
Normalize.normalize_email(raw, email)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# Mail Sunls 渠道实现
|
|
6
|
+
# API: https://mail.sunls.de
|
|
7
|
+
# 无需 token,无需 session
|
|
8
|
+
module MailSunls
|
|
9
|
+
CHANNEL = "mail-sunls"
|
|
10
|
+
BASE = "https://mail.sunls.de"
|
|
11
|
+
|
|
12
|
+
HEADERS = {
|
|
13
|
+
"Accept" => "application/json, text/plain, */*",
|
|
14
|
+
"Cache-Control" => "no-cache",
|
|
15
|
+
"DNT" => "1",
|
|
16
|
+
"Referer" => "https://mail.sunls.de/",
|
|
17
|
+
"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"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# 生成随机本地部分
|
|
23
|
+
def random_local(length = 10)
|
|
24
|
+
chars = ("a".."z").to_a + ("0".."9").to_a
|
|
25
|
+
Array.new(length) { chars.sample }.join
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 从 API 获取可用域名列表
|
|
29
|
+
def fetch_domains
|
|
30
|
+
resp = Http.get("#{BASE}/api/domain", headers: HEADERS, timeout: 15)
|
|
31
|
+
resp.raise_for_status
|
|
32
|
+
data = resp.json
|
|
33
|
+
raise "mail-sunls: 域名列表响应格式无效" unless data.is_a?(Array)
|
|
34
|
+
|
|
35
|
+
out = data.select { |d| d.is_a?(String) && !d.strip.empty? }.map(&:strip)
|
|
36
|
+
raise "mail-sunls: 无可用域名" if out.empty?
|
|
37
|
+
|
|
38
|
+
out
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 创建临时邮箱:获取域名列表后随机生成地址
|
|
42
|
+
# @return [EmailInfo]
|
|
43
|
+
def generate_email
|
|
44
|
+
domains = fetch_domains
|
|
45
|
+
domain = domains.sample
|
|
46
|
+
local = random_local(10)
|
|
47
|
+
EmailInfo.new(channel: CHANNEL, email: "#{local}@#{domain}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# 获取邮件列表
|
|
51
|
+
# @param email [String]
|
|
52
|
+
# @return [Array<Email>]
|
|
53
|
+
def get_emails(email)
|
|
54
|
+
addr = email.to_s.strip
|
|
55
|
+
raise "mail-sunls: 邮箱地址不能为空" if addr.empty?
|
|
56
|
+
|
|
57
|
+
resp = Http.get("#{BASE}/api/fetch?to=#{URI.encode_www_form_component(addr)}", headers: HEADERS, timeout: 15)
|
|
58
|
+
resp.raise_for_status
|
|
59
|
+
data = resp.json
|
|
60
|
+
return [] unless data.is_a?(Array)
|
|
61
|
+
|
|
62
|
+
data.select { |raw| raw.is_a?(Hash) }.map do |raw|
|
|
63
|
+
Normalize.normalize_email(raw, addr)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|