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,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # Nullmail 渠道实现(nullmail.cc / maildock.store)
8
+ #
9
+ # 无认证 REST:
10
+ # POST /api/emails(空 JSON body)建箱,响应
11
+ # {"address":"...@maildock.store","expiry":"2026-09-27T02:25:28.778Z"};
12
+ # GET /api/emails/{address}(URL 编码)读信,响应 {"expiry":"...","emails":[...]},
13
+ # 列表项只有 id/sender/subject/delivered,正文须逐封二拉 GET /api/emails/{addr}/body/{id}
14
+ # (响应 {"body":...})。
15
+ module Nullmail
16
+ CHANNEL = "nullmail"
17
+ BASE_URL = "https://www.nullmail.cc"
18
+ DOMAIN = "maildock.store"
19
+
20
+ HEADERS = {
21
+ "Accept" => "application/json",
22
+ "Origin" => BASE_URL,
23
+ "Referer" => "#{BASE_URL}/",
24
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
25
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
26
+ }.freeze
27
+
28
+ module_function
29
+
30
+ # 创建 nullmail 临时邮箱
31
+ # POST /api/emails(空 JSON body),token 复用完整地址以便收件箱回查
32
+ # @return [EmailInfo]
33
+ def generate_email
34
+ resp = Http.post("#{BASE_URL}/api/emails",
35
+ headers: HEADERS.merge("Content-Type" => "application/json"),
36
+ json: {}, timeout: 15)
37
+ resp.raise_for_status
38
+ data = resp.json
39
+ addr = data.is_a?(Hash) ? data["address"].to_s.strip : ""
40
+ raise "nullmail: 建箱响应缺少 address 字段" if addr.empty?
41
+
42
+ EmailInfo.new(channel: CHANNEL, email: addr, token: addr,
43
+ expires_at: data["expiry"])
44
+ end
45
+
46
+ # 单封正文二拉
47
+ # GET /api/emails/{addr}/body/{id},响应 {"body":"<完整纯文本正文>"}
48
+ # @param addr [String] 邮箱地址
49
+ # @param id [Object] 邮件 ID
50
+ # @return [String]
51
+ def fetch_body(addr, id)
52
+ uri = "#{BASE_URL}/api/emails/#{URI.encode_www_form_component(addr)}" \
53
+ "/body/#{URI.encode_www_form_component(id.to_s)}"
54
+ resp = Http.get(uri, headers: HEADERS, timeout: 15)
55
+ resp.raise_for_status
56
+ data = resp.json
57
+ data.is_a?(Hash) ? data["body"].to_s : ""
58
+ end
59
+
60
+ # 读取收件箱
61
+ # GET /api/emails/{address}(完整地址 URL 编码),列表项无正文,逐封二拉 body 端点;
62
+ # 正文拉取失败降级留空不阻断列表。
63
+ # @param email [String] 邮箱地址
64
+ # @param _token [String] 令牌(本渠道忽略,以邮箱为准)
65
+ # @return [Array<Email>]
66
+ def get_emails(email, _token)
67
+ addr = email.to_s.strip
68
+ raise "nullmail: 邮箱地址为空" if addr.empty?
69
+
70
+ uri = "#{BASE_URL}/api/emails/#{URI.encode_www_form_component(addr)}"
71
+ resp = Http.get(uri, headers: HEADERS, timeout: 15)
72
+ resp.raise_for_status
73
+ data = resp.json
74
+ emails = data.is_a?(Hash) ? data["emails"] : nil
75
+ return [] unless emails.is_a?(Array)
76
+
77
+ emails.filter_map do |m|
78
+ next unless m.is_a?(Hash)
79
+
80
+ row = {
81
+ "id" => m["id"],
82
+ "from" => m["sender"] || m["from"],
83
+ "to" => addr,
84
+ "subject" => m["subject"],
85
+ # 列表只有 id/sender/subject/delivered,delivered 显式映射为 date
86
+ "date" => m["delivered"] || m["date"]
87
+ }
88
+ body = fetch_body(addr, m["id"]) if m["id"]
89
+ row["text"] = body unless body.to_s.empty?
90
+ Normalize.normalize_email(row, addr)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # openinbox.io 渠道实现
8
+ #
9
+ # 流程:POST /api/inbox 创建邮箱,返回 id + email
10
+ # GET /api/emails/inbox/{id}?page=1&limit=50 获取邮件列表
11
+ # GET /api/emails/{messageId} 获取邮件详情
12
+ # token 存储 inbox id
13
+ module Openinbox
14
+ CHANNEL = "openinbox"
15
+ BASE_URL = "https://api.openinbox.io/api"
16
+
17
+ HEADERS = {
18
+ "Accept" => "application/json, text/plain, */*",
19
+ "Origin" => "https://openinbox.io",
20
+ "Referer" => "https://openinbox.io/",
21
+ "User-Agent" => "Mozilla/5.0"
22
+ }.freeze
23
+
24
+ module_function
25
+
26
+ # 发起 JSON 请求
27
+ # @param method [String] "GET" 或 "POST"
28
+ # @param url [String]
29
+ # @return [Object] 解析后的 JSON
30
+ def json_request(method, url)
31
+ hdrs = HEADERS.dup
32
+ hdrs["Content-Type"] = "application/json" if method == "POST"
33
+ resp = method == "POST" ? Http.post(url, headers: hdrs, timeout: 15) : Http.get(url, headers: hdrs, timeout: 15)
34
+ resp.raise_for_status
35
+ resp.json
36
+ end
37
+
38
+ # 将邮件 raw map 展平为标准字段
39
+ # @param raw [Hash]
40
+ # @param recipient [String]
41
+ # @return [Hash]
42
+ def flatten(raw, recipient)
43
+ {
44
+ "id" => raw["id"],
45
+ "from" => raw["from"],
46
+ "to" => recipient,
47
+ "subject" => raw["subject"],
48
+ "text" => raw["textBody"],
49
+ "html" => raw["htmlBody"],
50
+ "receivedAt" => raw["receivedAt"],
51
+ "isRead" => raw["isRead"],
52
+ "attachments" => []
53
+ }
54
+ end
55
+
56
+ # 创建 openinbox.io 临时邮箱
57
+ # @return [EmailInfo]
58
+ def generate_email
59
+ data = json_request("POST", BASE_URL + "/inbox")
60
+ id = data["id"].to_s.strip
61
+ email = data["email"].to_s.strip
62
+ raise "openinbox: 无效的邮箱响应" if id.empty? || email.empty? || !email.include?("@")
63
+
64
+ EmailInfo.new(channel: CHANNEL, email: email, token: id)
65
+ end
66
+
67
+ # 获取 openinbox.io 邮件列表
68
+ # @param inbox_id [String] inbox id(token)
69
+ # @param email [String]
70
+ # @return [Array<Email>]
71
+ def get_emails(inbox_id, email)
72
+ inbox_id = inbox_id.to_s.strip
73
+ raise "openinbox: empty inbox id" if inbox_id.empty?
74
+
75
+ url = "#{BASE_URL}/emails/inbox/#{URI.encode_uri_component(inbox_id)}?page=1&limit=50"
76
+ data = json_request("GET", url)
77
+ rows = data.is_a?(Hash) ? Array(data["emails"]) : []
78
+
79
+ rows.filter_map do |row|
80
+ next unless row.is_a?(Hash)
81
+
82
+ raw = row
83
+ msg_id = row["id"].to_s.strip
84
+ unless msg_id.empty?
85
+ detail = fetch_detail(msg_id)
86
+ raw = detail if detail.is_a?(Hash)
87
+ end
88
+ Normalize.normalize_email(flatten(raw, email), email)
89
+ end
90
+ end
91
+
92
+ # 获取单封邮件详情
93
+ # @param message_id [String]
94
+ # @return [Hash, nil]
95
+ def fetch_detail(message_id)
96
+ json_request("GET", "#{BASE_URL}/emails/#{URI.encode_uri_component(message_id)}")
97
+ rescue StandardError
98
+ nil
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TempmailSdk
4
+ module Providers
5
+ # Shadowmail 渠道实现(shadowmail.win)
6
+ #
7
+ # 完整接入契约:
8
+ # 注册 POST /api/register {"email":"<随机前缀>@gmail.com","password":"Abcd1234!"}
9
+ # → 200 {"message":"Successfully Registered"};
10
+ # 登录 POST /api/login → 200 {"message":"Successfull Login"}
11
+ # 并 Set-Cookie: sessionId=<uuid>(HttpOnly; Secure; Max-Age 3600);
12
+ # 建箱 POST /api/new-address → 200 {...,"address":"<id>@shadowmail.win","id":<id>};
13
+ # 读信 POST /api/get-emails {"address":"<地址>"} → 200 {"message":"Emails read",
14
+ # "mails":[...]};mails 元素字段 id/address_id/sender/subject/body/created_at。
15
+ # 会话隔离:全域使用显式 Cookie 头(sessionId=<uuid>),凭据串 token 持久化
16
+ # 注册邮箱/密码/会话 id,便于会话过期后自动重生。
17
+ module Shadowmail
18
+ CHANNEL = "shadowmail"
19
+ BASE_URL = "https://shadowmail.win"
20
+ # 固定注册密码(平台无自选密码入口,注册即固定)
21
+ PW = "Abcd1234!"
22
+ # 平台唯一收信域
23
+ DOMAIN = "shadowmail.win"
24
+ # 本渠道凭据串前缀
25
+ TOKEN_PREFIX = "shadowmail|"
26
+
27
+ HEADERS = {
28
+ "Content-Type" => "application/json",
29
+ "Accept" => "application/json",
30
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \
31
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
32
+ }.freeze
33
+
34
+ module_function
35
+
36
+ # 生成随机注册邮箱前缀(sdk+8 位小写字母)
37
+ # @return [String]
38
+ def random_account
39
+ chars = "abcdefghijklmnopqrstuvwxyz"
40
+ "sdk" + Array.new(8) { chars[rand(chars.length)] }.join
41
+ end
42
+
43
+ # 携带显式 Cookie 的 JSON POST 请求,返回 [body, status, set_cookies]
44
+ # @param path [String] 请求路径
45
+ # @param body [Hash] JSON 请求体
46
+ # @param cookie [String] Cookie 头(可为空)
47
+ # @return [Array(Array, Integer, Array<String>)]
48
+ def do_post(path, body, cookie)
49
+ headers = HEADERS.dup
50
+ headers["Cookie"] = cookie unless cookie.to_s.empty?
51
+ resp = Http.post("#{BASE_URL}#{path}", headers: headers, json: body, timeout: 15)
52
+ [resp.json, resp.status_code, resp.set_cookies]
53
+ end
54
+
55
+ # 从 Set-Cookie 中提取 sessionId 值(纯 uuid,不含键名)
56
+ # @param cookies [Array<String>]
57
+ # @return [String]
58
+ def session_from(cookies)
59
+ cookies.each do |line|
60
+ kv = line.split(";").first.to_s.strip
61
+ return kv.split("=", 2)[1].to_s if kv.start_with?("sessionId=")
62
+ end
63
+ ""
64
+ end
65
+
66
+ # 注册或登录(POST /api/register、/api/login),返回会话 sessionId
67
+ # @param account [String] 注册邮箱
68
+ # @param password [String] 密码
69
+ # @param is_login [Boolean] true 走登录,false 走注册
70
+ # @return [String] sessionId 值
71
+ def register_login(account, password, is_login)
72
+ path = is_login ? "/api/login" : "/api/register"
73
+ body = { "email" => account, "password" => password }
74
+ data, status, cookies = do_post(path, body, "")
75
+ raise "shadowmail #{path}: http #{status}" if status < 200 || status >= 300
76
+
77
+ msg = data.is_a?(Hash) ? data["message"].to_s : ""
78
+ if is_login
79
+ raise "shadowmail login: #{msg}" unless msg == "Successfull Login"
80
+ else
81
+ # 重复注册(幂等):消息为 Email already in use 时视为账号已存在,继续走登录
82
+ unless ["Successfully Registered", "Email already in use"].include?(msg)
83
+ raise "shadowmail register: #{msg}"
84
+ end
85
+ end
86
+ session = session_from(cookies)
87
+ if is_login && session.empty?
88
+ raise "shadowmail login: 未下发 sessionId Cookie"
89
+ end
90
+ session
91
+ end
92
+
93
+ # 注册账号并创建临时邮箱地址
94
+ # @return token 凭据串格式:"shadowmail|<account>|<password>|<sessionId>"
95
+ # @return [EmailInfo]
96
+ def generate_email
97
+ account = "#{random_account}@gmail.com"
98
+
99
+ # 1) 注册(幂等:已存在同名账号则跳过)
100
+ register_login(account, PW, false)
101
+ # 2) 登录取得 sessionId
102
+ session = register_login(account, PW, true)
103
+ # 3) 创建地址(每账号 12 槽):SDK 层显式 Cookie 头传 sessionId
104
+ data, status, = do_post("/api/new-address", {}, "sessionId=#{session}")
105
+ raise "shadowmail new-address: http #{status}" if status < 200 || status >= 300
106
+
107
+ address = data.is_a?(Hash) ? data["address"].to_s.strip : ""
108
+ raise "shadowmail new-address: 响应缺少有效地址" unless address.end_with?("@#{DOMAIN}")
109
+
110
+ # Token 持久化:account|password|sessionId(sessionId 为 uuid,无分隔符冲突)
111
+ token = TOKEN_PREFIX + [account, PW, session].join("|")
112
+ EmailInfo.new(channel: CHANNEL, email: address.downcase.strip, token: token)
113
+ end
114
+
115
+ # 解析凭据串为 account/password/sessionId 三元组
116
+ # @param token [String]
117
+ # @return [Array(String, String, String)]
118
+ def parse_token(token)
119
+ raise "shadowmail: token 格式错误" unless token.to_s.start_with?(TOKEN_PREFIX)
120
+
121
+ parts = token.to_s[TOKEN_PREFIX.length..].to_s.split("|", -1)
122
+ raise "shadowmail: token 字段缺失" unless parts.length == 3
123
+
124
+ account, password, session = parts
125
+ raise "shadowmail: token 凭据字段为空" if account.empty? || password.empty? || session.empty?
126
+
127
+ [account, password, session]
128
+ end
129
+
130
+ # 读取收件箱
131
+ # 会话失效时自动以凭据内 account/password 重新登录换新 sessionId。
132
+ # @param email [String] 平台新地址(<id>@shadowmail.win)
133
+ # @param token [String] 建箱下发的凭据串
134
+ # @return [Array<Email>]
135
+ def get_emails(email, token)
136
+ account, password, session = parse_token(token)
137
+ addr = email.to_s.strip
138
+
139
+ do_fetch = lambda do
140
+ do_post("/api/get-emails", { "address" => addr }, "sessionId=#{session}")
141
+ end
142
+
143
+ data, status, = do_fetch.call
144
+ # sessionId 最长 1 小时(Max-Age 3600),过期后重登录重试一次
145
+ if status == 401 || status == 404
146
+ new_session = register_login(account, password, true)
147
+ unless new_session.empty?
148
+ session = new_session
149
+ data, status, = do_fetch.call
150
+ end
151
+ end
152
+ raise "shadowmail get-emails: http #{status}" if status < 200 || status >= 300
153
+
154
+ msg = data.is_a?(Hash) ? data["message"].to_s : ""
155
+ raise "shadowmail get-emails: #{msg}" unless msg == "Emails read"
156
+
157
+ mails = data.is_a?(Hash) ? data["mails"] : nil
158
+ return [] unless mails.is_a?(Array)
159
+
160
+ mails.filter_map do |m|
161
+ next unless m.is_a?(Hash)
162
+
163
+ flat = m.dup
164
+ flat["from"] = m["sender"]
165
+ flat["to"] = addr
166
+ flat["date"] = m["created_at"]
167
+ # 平台无 text/html 区分,body 为正文(默认按纯文本处理,普通化可按需互转)
168
+ flat["text"] = m["body"]
169
+ Normalize.normalize_email(flat, addr)
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # shitpost.email 渠道实现(公共实例,克隆自 shamu4life/throwaway-email)
8
+ #
9
+ # 无认证 REST:
10
+ # POST /api/create 建箱(username/domain/ttl → email/token/type/expires)
11
+ # GET /api/inbox?email=&token= 读信(messages[] 含 from/fromName/subject/text/html/date)
12
+ # 域名池:shitpost.email / letsfuckingpiss.party
13
+ module ShitpostEmail
14
+ CHANNEL = "shitpost-email"
15
+ BASE_URL = "https://shitpost.email"
16
+ DOMAINS = %w[shitpost.email letsfuckingpiss.party].freeze
17
+
18
+ HEADERS = {
19
+ "Accept" => "application/json",
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
+ # 生成 "sdk"+10 位 [a-z0-9] 随机用户名
27
+ CHARS = ("a".."z").to_a + ("0".."9").to_a
28
+ def random_username
29
+ "sdk" + Array.new(10) { CHARS.sample }.join
30
+ end
31
+
32
+ # 创建临时邮箱
33
+ # POST /api/create,body 为 {username, domain, ttl}
34
+ # @return [EmailInfo]
35
+ def generate_email
36
+ body = { "username" => random_username, "domain" => DOMAINS.sample, "ttl" => 3600 }
37
+ resp = Http.post("#{BASE_URL}/api/create",
38
+ headers: HEADERS.merge("Content-Type" => "application/json"),
39
+ json: body, timeout: 15)
40
+ resp.raise_for_status
41
+ data = resp.json
42
+ email = data.is_a?(Hash) ? data["email"].to_s.strip : ""
43
+ token = data.is_a?(Hash) ? data["token"].to_s.strip : ""
44
+ raise "shitpost-email: 建箱响应缺少 email 或 token" if email.empty? || token.empty?
45
+
46
+ EmailInfo.new(channel: CHANNEL, email: email, token: token,
47
+ expires_at: data["expires"])
48
+ end
49
+
50
+ # 读取收件箱
51
+ # GET /api/inbox?email=&token=
52
+ # @param email [String] 邮箱地址
53
+ # @param token [String] 建箱返回的令牌
54
+ # @return [Array<Email>]
55
+ def get_emails(email, token)
56
+ addr = email.to_s.strip
57
+ tk = token.to_s
58
+ raise "shitpost-email: token 为空" if tk.strip.empty?
59
+
60
+ url = "#{BASE_URL}/api/inbox?email=#{URI.encode_www_form_component(addr)}" \
61
+ "&token=#{URI.encode_www_form_component(tk)}"
62
+ resp = Http.get(url, headers: HEADERS, timeout: 15)
63
+ resp.raise_for_status
64
+ data = resp.json
65
+ messages = data.is_a?(Hash) ? data["messages"] : nil
66
+ return [] unless messages.is_a?(Array)
67
+
68
+ messages.filter_map do |m|
69
+ next unless m.is_a?(Hash)
70
+
71
+ row = {
72
+ "id" => m["id"],
73
+ "from" => m["from"],
74
+ "to" => addr,
75
+ "subject" => m["subject"],
76
+ "text" => m["text"],
77
+ "html" => m["html"],
78
+ "date" => m["date"]
79
+ }
80
+ Normalize.normalize_email(row, addr)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "uri"
5
+
6
+ module TempmailSdk
7
+ module Providers
8
+ # smail.pw 渠道实现
9
+ #
10
+ # 流程:POST /_root.data body: intent=generate 创建邮箱,保存 __session cookie
11
+ # GET /_root.data + Cookie 获取邮件列表(解析 React Flight 格式或正则提取)
12
+ # GET /api/email/{id} 获取单封邮件正文
13
+ # token 存储 __session=xxx cookie 字符串
14
+ module SmailPw
15
+ CHANNEL = "smail-pw"
16
+ ROOT_DATA_URL = "https://smail.pw/_root.data"
17
+
18
+ HEADERS = {
19
+ "Accept" => "*/*",
20
+ "accept-language" => "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
21
+ "cache-control" => "no-cache",
22
+ "dnt" => "1",
23
+ "origin" => "https://smail.pw",
24
+ "pragma" => "no-cache",
25
+ "referer" => "https://smail.pw/",
26
+ "sec-fetch-dest" => "empty",
27
+ "sec-fetch-mode" => "cors",
28
+ "sec-fetch-site" => "same-origin",
29
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
30
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
31
+ }.freeze
32
+
33
+ QUOTED_INBOX_RE = /"([a-z0-9][a-z0-9.\-]*@smail\.pw)"/i
34
+ PLAIN_INBOX_RE = /\b([a-z0-9][a-z0-9.\-]*@smail\.pw)\b/i
35
+ MAIL_BLOCK_RE = /"id","([^"]+)","to_address","([^"]*)","from_name","([^"]*)","from_address","([^"]*)","subject","([^"]*)","time",(\d+)/
36
+ MAIL_BLOCK2_RE = /"id","([^"]+)","from_name","([^"]*)","from_address","([^"]*)","subject","([^"]*)","time",(\d+)/
37
+
38
+ module_function
39
+
40
+ # 从响应 Set-Cookie 中提取 __session cookie
41
+ # @param set_cookie_lines [Array<String>]
42
+ # @return [String]
43
+ def extract_session(set_cookie_lines)
44
+ set_cookie_lines.each do |line|
45
+ pair = line.split(";", 2).first.to_s.strip
46
+ return pair if pair.start_with?("__session=")
47
+ end
48
+ ""
49
+ end
50
+
51
+ # 从响应体中提取邮箱地址
52
+ # @param body [String]
53
+ # @return [String]
54
+ def extract_inbox(body)
55
+ m = body.match(QUOTED_INBOX_RE)
56
+ return m[1] if m
57
+
58
+ m = body.match(PLAIN_INBOX_RE)
59
+ m ? m[1] : ""
60
+ end
61
+
62
+ # 从响应体解析邮件列表
63
+ # @param body [String]
64
+ # @param recipient [String]
65
+ # @return [Array<Hash>]
66
+ def parse_mails(body, recipient)
67
+ out = []
68
+ body.scan(MAIL_BLOCK_RE) do |id, to_addr, from_name, from_addr, subj, ts|
69
+ out << {
70
+ "id" => id, "to_address" => to_addr.empty? ? recipient : to_addr,
71
+ "from_name" => from_name, "from_address" => from_addr,
72
+ "subject" => subj, "date" => ts.to_f,
73
+ "text" => "", "html" => "", "attachments" => []
74
+ }
75
+ end
76
+ return out unless out.empty?
77
+
78
+ body.scan(MAIL_BLOCK2_RE) do |id, from_name, from_addr, subj, ts|
79
+ out << {
80
+ "id" => id, "to_address" => recipient,
81
+ "from_name" => from_name, "from_address" => from_addr,
82
+ "subject" => subj, "date" => ts.to_f,
83
+ "text" => "", "html" => "", "attachments" => []
84
+ }
85
+ end
86
+ out
87
+ end
88
+
89
+ # 创建 smail.pw 临时邮箱
90
+ # @return [EmailInfo]
91
+ def generate_email
92
+ resp = Http.post(
93
+ ROOT_DATA_URL,
94
+ headers: HEADERS.merge("Content-Type" => "application/x-www-form-urlencoded;charset=UTF-8"),
95
+ body: "intent=generate",
96
+ timeout: 15
97
+ )
98
+ raise "smail.pw generate failed: #{resp.status_code}" unless resp.ok?
99
+
100
+ cookie = extract_session(resp.set_cookies)
101
+ raise "smail.pw: 未能提取 __session cookie" if cookie.empty?
102
+
103
+ email = extract_inbox(resp.body)
104
+ raise "smail.pw: 未能解析邮箱地址" if email.empty?
105
+
106
+ EmailInfo.new(channel: CHANNEL, email: email, token: cookie)
107
+ end
108
+
109
+ # 获取 smail.pw 邮件列表
110
+ # @param token [String] __session=xxx cookie 字符串
111
+ # @param email [String]
112
+ # @return [Array<Email>]
113
+ def get_emails(token, email)
114
+ resp = Http.get(
115
+ ROOT_DATA_URL,
116
+ headers: HEADERS.merge("Cookie" => token),
117
+ timeout: 15
118
+ )
119
+ raise "smail.pw get emails failed: #{resp.status_code}" unless resp.ok?
120
+
121
+ mails = parse_mails(resp.body, email)
122
+ return [] if mails.empty?
123
+
124
+ mails.map do |m|
125
+ ne = Normalize.normalize_email(m, email)
126
+ id = m["id"].to_s
127
+ unless id.empty?
128
+ body_html = fetch_body(token, id)
129
+ ne.html = body_html unless body_html.empty?
130
+ end
131
+ ne
132
+ end
133
+ end
134
+
135
+ # 获取单封邮件正文
136
+ # @param token [String]
137
+ # @param id [String]
138
+ # @return [String]
139
+ def fetch_body(token, id)
140
+ return "" if id.empty?
141
+
142
+ resp = Http.get(
143
+ "https://smail.pw/api/email/#{URI.encode_uri_component(id)}",
144
+ headers: HEADERS.merge("Cookie" => token, "Accept" => "application/json"),
145
+ timeout: 15
146
+ )
147
+ return "" unless resp.ok?
148
+
149
+ data = resp.json
150
+ data.is_a?(Hash) ? data["body"].to_s : ""
151
+ rescue StandardError
152
+ ""
153
+ end
154
+ end
155
+ end
156
+ end