tempmail_sdk 1.3.3 → 1.3.4

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/tempmail_sdk/providers/altmails.rb +99 -0
  3. data/lib/tempmail_sdk/providers/apihz.rb +138 -0
  4. data/lib/tempmail_sdk/providers/awamail.rb +95 -0
  5. data/lib/tempmail_sdk/providers/best_temp_mail.rb +77 -0
  6. data/lib/tempmail_sdk/providers/chatgpt_org_uk.rb +129 -0
  7. data/lib/tempmail_sdk/providers/disposablemail.rb +125 -0
  8. data/lib/tempmail_sdk/providers/disposablemail_app.rb +66 -0
  9. data/lib/tempmail_sdk/providers/email10min.rb +152 -0
  10. data/lib/tempmail_sdk/providers/emailnator.rb +132 -0
  11. data/lib/tempmail_sdk/providers/emailtemp_org.rb +131 -0
  12. data/lib/tempmail_sdk/providers/expressinboxhub.rb +118 -0
  13. data/lib/tempmail_sdk/providers/fakemail.rb +143 -0
  14. data/lib/tempmail_sdk/providers/haribu.rb +29 -3
  15. data/lib/tempmail_sdk/providers/linshiyouxiang_net.rb +85 -0
  16. data/lib/tempmail_sdk/providers/mail_sunls.rb +42 -1
  17. data/lib/tempmail_sdk/providers/mail_td.rb +161 -0
  18. data/lib/tempmail_sdk/providers/mailcat_ai.rb +52 -0
  19. data/lib/tempmail_sdk/providers/maildrop.rb +79 -4
  20. data/lib/tempmail_sdk/providers/mailgolem.rb +96 -0
  21. data/lib/tempmail_sdk/providers/mailinator.rb +9 -2
  22. data/lib/tempmail_sdk/providers/mailtemp_cc.rb +91 -0
  23. data/lib/tempmail_sdk/providers/minuteinbox.rb +174 -0
  24. data/lib/tempmail_sdk/providers/moakt.rb +29 -3
  25. data/lib/tempmail_sdk/providers/mohmal.rb +208 -0
  26. data/lib/tempmail_sdk/providers/mytempmail_cc.rb +76 -0
  27. data/lib/tempmail_sdk/providers/openinbox.rb +102 -0
  28. data/lib/tempmail_sdk/providers/smail_pw.rb +156 -0
  29. data/lib/tempmail_sdk/providers/socketio_mail.rb +25 -0
  30. data/lib/tempmail_sdk/providers/tempgbox.rb +118 -0
  31. data/lib/tempmail_sdk/providers/tempmail_cn.rb +111 -0
  32. data/lib/tempmail_sdk/providers/tenminutemail_net.rb +183 -0
  33. data/lib/tempmail_sdk/providers/twentyfourmail_chacuo.rb +83 -0
  34. data/lib/tempmail_sdk/providers/vip_215.rb +171 -0
  35. data/lib/tempmail_sdk/registry.rb +163 -1
  36. data/lib/tempmail_sdk/version.rb +1 -1
  37. metadata +30 -2
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # disposablemail.app 渠道实现
8
+ #
9
+ # 纯 REST JSON API,无需认证。
10
+ # 创建收件箱: POST /api/inbox (body: {})
11
+ # 获取邮件: GET /api/inbox/emails?token={token}
12
+ module DisposablemailApp
13
+ CHANNEL = "disposablemail-app"
14
+ API_BASE = "https://disposablemail.app/api"
15
+
16
+ HEADERS = {
17
+ "Accept" => "application/json, text/plain, */*",
18
+ "Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
19
+ "Content-Type" => "application/json",
20
+ "Origin" => "https://disposablemail.app",
21
+ "Referer" => "https://disposablemail.app/",
22
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
23
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
24
+ }.freeze
25
+
26
+ module_function
27
+
28
+ # 创建临时邮箱
29
+ # @return [EmailInfo]
30
+ def generate_email
31
+ resp = Http.post("#{API_BASE}/inbox", headers: HEADERS, body: "{}")
32
+ resp.raise_for_status
33
+ data = resp.json
34
+ address = data["address"].to_s.strip
35
+ token = data["token"].to_s.strip
36
+ raise "disposablemail-app: address 或 token 为空" if address.empty? || token.empty?
37
+
38
+ info = EmailInfo.new(channel: CHANNEL, email: address, token: token)
39
+ info.expires_at = data["expiresAt"].to_s
40
+ info.created_at = data["createdAt"].to_s
41
+ info
42
+ end
43
+
44
+ # 获取邮件列表
45
+ # @param email [String]
46
+ # @param token [String] API 返回的 token
47
+ # @return [Array<Email>]
48
+ def get_emails(email, token)
49
+ raise "disposablemail-app: token 为空" if token.to_s.empty?
50
+
51
+ get_headers = HEADERS.reject { |k, _| k == "Content-Type" }
52
+ resp = Http.get("#{API_BASE}/inbox/emails?token=#{token}", headers: get_headers)
53
+ resp.raise_for_status
54
+ data = resp.json
55
+ emails_raw = data["emails"]
56
+ return [] unless emails_raw.is_a?(Array) && !emails_raw.empty?
57
+
58
+ emails_raw.filter_map do |raw|
59
+ next unless raw.is_a?(Hash)
60
+
61
+ Normalize.normalize_email(raw, email)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "json"
5
+
6
+ module TempmailSdk
7
+ module Providers
8
+ # email10min.com 渠道实现
9
+ #
10
+ # 流程:
11
+ # 1. GET /zh 获取 CSRF meta token + session cookies + HTML 中的邮箱地址
12
+ # 2. POST /messages?<ts> 携带 Cookie + _token 表单 获取邮件列表
13
+ # token 格式: "e10m:" + base64(JSON{c: cookie, t: csrf})
14
+ module Email10min
15
+ CHANNEL = "email10min"
16
+ BASE_URL = "https://email10min.com"
17
+ TOK_PREFIX = "e10m:"
18
+
19
+ CSRF_META_RE = /<meta\s+name="csrf-token"\s+content="([^"]+)"/.freeze
20
+ CSRF_INPUT_RE = /<input[^>]+name="_token"[^>]+value="([^"]+)"/.freeze
21
+ EMAIL_ID_RE = /id="emailAddress"[^>]*>([^<]+)/.freeze
22
+ EMAIL_DATA_RE = /data-email="([^"]+@[^"]+)"/.freeze
23
+ EMAIL_JSON_RE = /"mailbox"\s*:\s*"([^"]+@[^"]+)"/.freeze
24
+ EMAIL_GENERIC_RE = /([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})/.freeze
25
+
26
+ BROWSER_HEADERS = {
27
+ "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
28
+ "Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
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
+ AJAX_HEADERS = {
34
+ "Accept" => "application/json, text/plain, */*",
35
+ "Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
36
+ "Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8",
37
+ "Origin" => BASE_URL,
38
+ "Referer" => "#{BASE_URL}/zh",
39
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
40
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
41
+ "X-Requested-With" => "XMLHttpRequest"
42
+ }.freeze
43
+
44
+ module_function
45
+
46
+ # 从响应的 Set-Cookie 头合并为 Cookie 字符串
47
+ def merge_cookies(resp)
48
+ parts = []
49
+ resp.set_cookies.each do |line|
50
+ first = line.split(";", 2).first.to_s.strip
51
+ parts << first unless first.empty?
52
+ end
53
+ parts.join("; ")
54
+ end
55
+
56
+ # token 编码
57
+ def encode_token(cookie, csrf)
58
+ raw = JSON.generate("c" => cookie, "t" => csrf)
59
+ TOK_PREFIX + Base64.strict_encode64(raw)
60
+ end
61
+
62
+ # token 解码,返回 [cookie, csrf]
63
+ def decode_token(token)
64
+ raise "email10min: 无效 session token" unless token.to_s.start_with?(TOK_PREFIX)
65
+
66
+ raw = Base64.strict_decode64(token[TOK_PREFIX.length..])
67
+ data = JSON.parse(raw)
68
+ cookie = data["c"].to_s
69
+ csrf = data["t"].to_s
70
+ raise "email10min: 无效 session token(字段缺失)" if cookie.empty? || csrf.empty?
71
+
72
+ [cookie, csrf]
73
+ rescue ArgumentError, JSON::ParserError
74
+ raise "email10min: 无效 session token"
75
+ end
76
+
77
+ # 从 HTML 中提取 CSRF token
78
+ def extract_csrf(html)
79
+ m = html.match(CSRF_META_RE) || html.match(CSRF_INPUT_RE)
80
+ raise "email10min: 未找到 CSRF token" unless m
81
+
82
+ m[1]
83
+ end
84
+
85
+ # 从 HTML 中提取邮箱地址
86
+ def extract_email(html)
87
+ [EMAIL_ID_RE, EMAIL_DATA_RE, EMAIL_JSON_RE].each do |re|
88
+ if (m = html.match(re))
89
+ addr = m[1].strip
90
+ return addr if addr.include?("@")
91
+ end
92
+ end
93
+ if (m = html.match(EMAIL_GENERIC_RE))
94
+ addr = m[1].strip
95
+ return addr unless addr.include?("email10min") || addr.include?("example")
96
+ end
97
+ raise "email10min: 未找到邮箱地址"
98
+ end
99
+
100
+ # 创建临时邮箱
101
+ # @return [EmailInfo]
102
+ def generate_email
103
+ resp = Http.get("#{BASE_URL}/zh", headers: BROWSER_HEADERS)
104
+ resp.raise_for_status
105
+ cookie = merge_cookies(resp)
106
+ html = resp.body
107
+ csrf = extract_csrf(html)
108
+ email = extract_email(html)
109
+ EmailInfo.new(channel: CHANNEL, email: email, token: encode_token(cookie, csrf))
110
+ end
111
+
112
+ # 获取邮件列表
113
+ # @param email [String]
114
+ # @param token [String]
115
+ # @return [Array<Email>]
116
+ def get_emails(email, token)
117
+ cookie, csrf = decode_token(token)
118
+ ts = (Time.now.to_f * 1000).to_i
119
+ body = "_token=#{csrf}&captcha="
120
+ resp = Http.post("#{BASE_URL}/messages?#{ts}",
121
+ headers: AJAX_HEADERS.merge("Cookie" => cookie),
122
+ body: body)
123
+ resp.raise_for_status
124
+ data = JSON.parse(resp.body)
125
+ messages = data["messages"]
126
+ return [] unless messages.is_a?(Array) && !messages.empty?
127
+
128
+ messages.each_with_index.map do |msg, idx|
129
+ id = msg["id"] || msg["message_id"] || idx
130
+ from_addr = msg["from"].to_s
131
+ from_addr = msg["sender"].to_s if from_addr.empty?
132
+ text = msg["text"].to_s
133
+ text = msg["body"].to_s if text.empty?
134
+ html_body = msg["html"].to_s
135
+ html_body = msg["body_html"].to_s if html_body.empty?
136
+ date = msg["date"].to_s
137
+ date = msg["created_at"].to_s if date.empty?
138
+ Normalize.normalize_email({
139
+ "id" => id.to_s,
140
+ "from" => from_addr,
141
+ "to" => msg["to"].to_s.empty? ? email : msg["to"],
142
+ "subject" => msg["subject"].to_s,
143
+ "text" => text,
144
+ "html" => html_body,
145
+ "date" => date,
146
+ "isRead" => false
147
+ }, email)
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "uri"
5
+
6
+ module TempmailSdk
7
+ module Providers
8
+ # emailnator.com 渠道实现
9
+ #
10
+ # 流程:
11
+ # 1. GET / 获取 XSRF-TOKEN cookie(需 URL 解码)和 session cookie
12
+ # 2. POST /generate-email {"email":["plusGmail","dotGmail","googleMail"]} 创建邮箱
13
+ # 3. POST /message-list {"email":"..."} 获取邮件列表
14
+ # 4. POST /message-list {"email":"...","messageID":"..."} 获取单封 HTML
15
+ # token 存储 JSON {"xsrfToken":"...","cookie":"..."}
16
+ module Emailnator
17
+ CHANNEL = "emailnator"
18
+ BASE_URL = "https://www.emailnator.com"
19
+ OPTIONS = %w[plusGmail dotGmail googleMail].freeze
20
+
21
+ BROWSER_HEADERS = {
22
+ "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
23
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
24
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
25
+ }.freeze
26
+
27
+ AJAX_HEADERS = {
28
+ "Accept" => "application/json, text/plain, */*",
29
+ "Content-Type" => "application/json",
30
+ "Origin" => BASE_URL,
31
+ "Referer" => "#{BASE_URL}/",
32
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
33
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
34
+ "X-Requested-With" => "XMLHttpRequest"
35
+ }.freeze
36
+
37
+ module_function
38
+
39
+ # 从响应的 Set-Cookie 头合并为 Cookie 字符串
40
+ def merge_cookies(resp)
41
+ parts = []
42
+ resp.set_cookies.each do |line|
43
+ first = line.split(";", 2).first.to_s.strip
44
+ parts << first unless first.empty?
45
+ end
46
+ parts.join("; ")
47
+ end
48
+
49
+ # 初始化 session:GET / 建立 XSRF-TOKEN 与 session cookie
50
+ # @return [Hash] {"xsrfToken" => ..., "cookie" => ...}
51
+ def init_session
52
+ resp = Http.get(BASE_URL, headers: BROWSER_HEADERS)
53
+ resp.raise_for_status
54
+ cookie = merge_cookies(resp)
55
+ xsrf = resp.cookie_value("XSRF-TOKEN").to_s
56
+ xsrf = URI.decode_www_form_component(xsrf) unless xsrf.empty?
57
+ raise "emailnator: 缺少 session cookie 或 XSRF token" if cookie.empty? || xsrf.empty?
58
+
59
+ { "xsrfToken" => xsrf, "cookie" => cookie }
60
+ end
61
+
62
+ # 携带 session 发送 POST JSON 请求,返回原始 body
63
+ def session_post(session, path, body)
64
+ resp = Http.post("#{BASE_URL}#{path}",
65
+ headers: AJAX_HEADERS.merge(
66
+ "Cookie" => session["cookie"],
67
+ "X-XSRF-TOKEN" => session["xsrfToken"]
68
+ ),
69
+ json: body)
70
+ resp.raise_for_status
71
+ resp.body
72
+ end
73
+
74
+ # 创建临时邮箱
75
+ # @return [EmailInfo]
76
+ def generate_email
77
+ session = init_session
78
+ raw = session_post(session, "/generate-email", { "email" => OPTIONS })
79
+ data = JSON.parse(raw)
80
+ emails_arr = data["email"]
81
+ email = emails_arr.is_a?(Array) ? emails_arr.find { |e| !e.to_s.empty? } : nil
82
+ raise "emailnator: 创建邮箱返回为空" if email.nil? || email.empty?
83
+
84
+ EmailInfo.new(channel: CHANNEL, email: email, token: JSON.generate(session))
85
+ end
86
+
87
+ # 获取邮件列表
88
+ # @param email [String]
89
+ # @param token [String] JSON {"xsrfToken":"...","cookie":"..."}
90
+ # @return [Array<Email>]
91
+ def get_emails(email, token)
92
+ session = JSON.parse(token)
93
+ raise "emailnator: 无效 session token" \
94
+ if session["cookie"].to_s.empty? || session["xsrfToken"].to_s.empty?
95
+
96
+ raw = session_post(session, "/message-list", { "email" => email })
97
+ data = JSON.parse(raw)
98
+ rows = data["messageData"]
99
+ return [] unless rows.is_a?(Array) && !rows.empty?
100
+
101
+ rows.filter_map do |row|
102
+ next unless row.is_a?(Hash)
103
+
104
+ mid = row["messageID"].to_s
105
+ next if mid.empty? || mid.start_with?("ADS")
106
+
107
+ html_body = fetch_detail(session, email, mid)
108
+ Normalize.normalize_email({
109
+ "id" => mid,
110
+ "from" => row["from"].to_s,
111
+ "to" => email,
112
+ "subject" => row["subject"].to_s,
113
+ "html" => html_body,
114
+ "date" => row["time"].to_s,
115
+ "isRead" => false
116
+ }, email)
117
+ end
118
+ rescue JSON::ParserError
119
+ raise "emailnator: 无效 session token"
120
+ end
121
+
122
+ # 拉取单封邮件详情 HTML
123
+ def fetch_detail(session, email, message_id)
124
+ raw = session_post(session, "/message-list", { "email" => email, "messageID" => message_id })
125
+ # 详情响应可能是 JSON 编码的 HTML 字符串,也可能是原始 HTML
126
+ JSON.parse(raw)
127
+ rescue StandardError
128
+ raw.to_s
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "uri"
5
+
6
+ module TempmailSdk
7
+ module Providers
8
+ # emailtemp.org 渠道实现(与 tempmailten.com 共享 Laravel 模板)
9
+ #
10
+ # 流程:
11
+ # 1. GET /en 获取 session cookie + CSRF meta token
12
+ # 2. POST /messages(form: _token={csrf}&captcha=)获取邮箱地址和邮件列表
13
+ # 3. GET /view/{id} 获取单封邮件 HTML 正文
14
+ # token 存储 "cookie\ncsrf"
15
+ module EmailtempOrg
16
+ CHANNEL = "emailtemp-org"
17
+ BASE_URL = "https://emailtemp.org"
18
+
19
+ CSRF_RE = /<meta\s+name="csrf-token"\s+content="([^"]+)"/.freeze
20
+
21
+ BROWSER_HEADERS = {
22
+ "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
23
+ "Accept-Language" => "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
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"
26
+ }.freeze
27
+
28
+ AJAX_HEADERS = {
29
+ "Accept" => "application/json, text/javascript, */*; q=0.01",
30
+ "Accept-Language" => "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
31
+ "Content-Type" => "application/x-www-form-urlencoded",
32
+ "Referer" => "#{BASE_URL}/en",
33
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
34
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
35
+ "X-Requested-With" => "XMLHttpRequest"
36
+ }.freeze
37
+
38
+ module_function
39
+
40
+ # 合并 Set-Cookie 为 Cookie 字符串
41
+ def merge_cookies(resp)
42
+ parts = []
43
+ resp.set_cookies.each do |line|
44
+ first = line.split(";", 2).first.to_s.strip
45
+ parts << first unless first.empty?
46
+ end
47
+ parts.join("; ")
48
+ end
49
+
50
+ # POST /messages 携带 CSRF 与 Cookie
51
+ def post_messages(cookie, csrf)
52
+ form = "_token=#{URI.encode_www_form_component(csrf)}&captcha="
53
+ headers = AJAX_HEADERS.merge("X-CSRF-TOKEN" => csrf)
54
+ headers = headers.merge("Cookie" => cookie) unless cookie.to_s.empty?
55
+ Http.post("#{BASE_URL}/messages", headers: headers, body: form)
56
+ end
57
+
58
+ # 创建临时邮箱
59
+ # @return [EmailInfo]
60
+ def generate_email
61
+ r1 = Http.get("#{BASE_URL}/en", headers: BROWSER_HEADERS)
62
+ r1.raise_for_status
63
+ cookie = merge_cookies(r1)
64
+ m = r1.body.match(CSRF_RE)
65
+ raise "emailtemp-org: 未能从首页提取 CSRF token" unless m
66
+
67
+ csrf = m[1]
68
+
69
+ r2 = post_messages(cookie, csrf)
70
+ r2.raise_for_status
71
+ new_cookie = merge_cookies(r2)
72
+ cookie = new_cookie unless new_cookie.empty?
73
+
74
+ data = JSON.parse(r2.body)
75
+ email = data["mailbox"].to_s.strip
76
+ raise "emailtemp-org: 邮箱地址无效: #{email.inspect}" if email.empty? || !email.include?("@")
77
+
78
+ token = "#{cookie}\n#{csrf}"
79
+ EmailInfo.new(channel: CHANNEL, email: email, token: token)
80
+ end
81
+
82
+ # 获取邮件列表
83
+ # @param email [String]
84
+ # @param token [String] "cookie\ncsrf"
85
+ # @return [Array<Email>]
86
+ def get_emails(email, token)
87
+ raise "emailtemp-org: 邮箱地址为空" if email.to_s.strip.empty?
88
+
89
+ cookie, csrf = token.to_s.split("\n", 2)
90
+ csrf = cookie if csrf.nil? # 兼容旧 token 仅存 CSRF 的情形
91
+ cookie = "" if csrf.nil? || csrf == cookie
92
+ raise "emailtemp-org: 缺少 CSRF token" if csrf.to_s.empty?
93
+
94
+ resp = post_messages(cookie, csrf)
95
+ resp.raise_for_status
96
+ data = JSON.parse(resp.body)
97
+ messages = data["messages"]
98
+ return [] unless messages.is_a?(Array) && !messages.empty?
99
+
100
+ messages.map do |msg|
101
+ id = msg["id"].to_s
102
+ from_addr = msg["from_email"].to_s
103
+ if !msg["from"].to_s.empty? && msg["from"] != from_addr
104
+ from_addr = "#{msg['from']} <#{msg['from_email']}>"
105
+ end
106
+ html_body = fetch_view(cookie, id)
107
+ Normalize.normalize_email({
108
+ "id" => id,
109
+ "from" => from_addr,
110
+ "to" => email,
111
+ "subject" => msg["subject"].to_s,
112
+ "html" => html_body,
113
+ "isRead" => msg["is_seen"] == true
114
+ }, email)
115
+ end
116
+ end
117
+
118
+ # 获取单封邮件 HTML 正文
119
+ def fetch_view(cookie, id)
120
+ return "" if id.to_s.empty?
121
+
122
+ headers = BROWSER_HEADERS.merge("Referer" => "#{BASE_URL}/en")
123
+ headers = headers.merge("Cookie" => cookie) unless cookie.to_s.empty?
124
+ resp = Http.get("#{BASE_URL}/view/#{URI.encode_www_form_component(id)}", headers: headers)
125
+ resp.ok? ? resp.body : ""
126
+ rescue StandardError
127
+ ""
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "uri"
5
+
6
+ module TempmailSdk
7
+ module Providers
8
+ # expressinboxhub.com 渠道实现
9
+ #
10
+ # 流程:
11
+ # 1. GET / 获取 CSRF meta token + session cookies
12
+ # 2. POST /messages(form: _token={csrf})创建邮箱并返回消息列表
13
+ # token 格式:"cookie\ncsrf"
14
+ module Expressinboxhub
15
+ CHANNEL = "expressinboxhub"
16
+ BASE_URL = "https://expressinboxhub.com"
17
+
18
+ CSRF_RE = /<meta\s+name="csrf-token"\s+content="([^"]+)"/.freeze
19
+
20
+ BROWSER_HEADERS = {
21
+ "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
22
+ "Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
23
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
24
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
25
+ }.freeze
26
+
27
+ AJAX_HEADERS = {
28
+ "Accept" => "application/json, text/plain, */*",
29
+ "Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
30
+ "Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8",
31
+ "Origin" => BASE_URL,
32
+ "Referer" => "#{BASE_URL}/",
33
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
34
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
35
+ "X-Requested-With" => "XMLHttpRequest"
36
+ }.freeze
37
+
38
+ module_function
39
+
40
+ # 合并 Set-Cookie 为 Cookie 字符串
41
+ def merge_cookies(existing, resp)
42
+ parts = existing.to_s.empty? ? [] : [existing]
43
+ resp.set_cookies.each do |line|
44
+ first = line.split(";", 2).first.to_s.strip
45
+ parts << first unless first.empty?
46
+ end
47
+ parts.join("; ")
48
+ end
49
+
50
+ # POST /messages 携带 CSRF 与 Cookie
51
+ def post_messages(cookie, csrf)
52
+ headers = AJAX_HEADERS.merge("X-CSRF-TOKEN" => csrf)
53
+ headers = headers.merge("Cookie" => cookie) unless cookie.to_s.empty?
54
+ Http.post("#{BASE_URL}/messages", headers: headers, body: "_token=#{URI.encode_www_form_component(csrf)}")
55
+ end
56
+
57
+ # 创建临时邮箱
58
+ # @return [EmailInfo]
59
+ def generate_email
60
+ r1 = Http.get(BASE_URL, headers: BROWSER_HEADERS)
61
+ r1.raise_for_status
62
+ cookie = merge_cookies("", r1)
63
+ m = r1.body.match(CSRF_RE)
64
+ raise "expressinboxhub: 未找到 CSRF token" unless m
65
+
66
+ csrf = m[1]
67
+
68
+ r2 = post_messages(cookie, csrf)
69
+ r2.raise_for_status
70
+ cookie = merge_cookies(cookie, r2)
71
+ data = JSON.parse(r2.body)
72
+ email = data["mailbox"].to_s.strip
73
+ raise "expressinboxhub: 响应中未包含邮箱地址" if email.empty?
74
+
75
+ EmailInfo.new(channel: CHANNEL, email: email, token: "#{cookie}\n#{csrf}")
76
+ end
77
+
78
+ # 获取邮件列表
79
+ # @param email [String]
80
+ # @param token [String] "cookie\ncsrf"
81
+ # @return [Array<Email>]
82
+ def get_emails(email, token)
83
+ cookie, csrf = token.to_s.split("\n", 2)
84
+ raise "expressinboxhub: 无效的 session token" if cookie.to_s.empty? || csrf.to_s.empty?
85
+
86
+ resp = post_messages(cookie, csrf)
87
+ resp.raise_for_status
88
+ data = JSON.parse(resp.body)
89
+ messages = data["messages"]
90
+ return [] unless messages.is_a?(Array) && !messages.empty?
91
+
92
+ messages.each_with_index.map do |msg, idx|
93
+ id = msg["id"].to_s
94
+ id = idx.to_s if id.empty?
95
+ from_addr = msg["from"].to_s
96
+ from_addr = msg["sender"].to_s if from_addr.empty?
97
+ to_addr = msg["to"].to_s
98
+ to_addr = email if to_addr.empty?
99
+ date = msg["receivedAt"].to_s
100
+ date = msg["date"].to_s if date.empty?
101
+ date = msg["created_at"].to_s if date.empty?
102
+ text = msg["text"].to_s
103
+ text = msg["body"].to_s if text.empty?
104
+ Normalize.normalize_email({
105
+ "id" => id,
106
+ "from" => from_addr,
107
+ "to" => to_addr,
108
+ "subject" => msg["subject"].to_s,
109
+ "text" => text,
110
+ "html" => msg["content"].to_s,
111
+ "date" => date,
112
+ "isRead" => false
113
+ }, email)
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end