react-native-email-imap-smtp 0.2.0 → 0.2.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-email-imap-smtp",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "The best of imap and smtp client of react native",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -7,6 +7,33 @@
7
7
  <Project ToolsVersion="Current" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
8
8
  <Import Project="$(MSBuildThisFileDirectory)..\ExperimentalFeatures.props" Condition="Exists('$(MSBuildThisFileDirectory)..\ExperimentalFeatures.props')" />
9
9
 
10
+ <ItemGroup Label="ProjectConfigurations">
11
+ <ProjectConfiguration Include="Debug|ARM64">
12
+ <Configuration>Debug</Configuration>
13
+ <Platform>ARM64</Platform>
14
+ </ProjectConfiguration>
15
+ <ProjectConfiguration Include="Debug|Win32">
16
+ <Configuration>Debug</Configuration>
17
+ <Platform>Win32</Platform>
18
+ </ProjectConfiguration>
19
+ <ProjectConfiguration Include="Debug|x64">
20
+ <Configuration>Debug</Configuration>
21
+ <Platform>x64</Platform>
22
+ </ProjectConfiguration>
23
+ <ProjectConfiguration Include="Release|ARM64">
24
+ <Configuration>Release</Configuration>
25
+ <Platform>ARM64</Platform>
26
+ </ProjectConfiguration>
27
+ <ProjectConfiguration Include="Release|Win32">
28
+ <Configuration>Release</Configuration>
29
+ <Platform>Win32</Platform>
30
+ </ProjectConfiguration>
31
+ <ProjectConfiguration Include="Release|x64">
32
+ <Configuration>Release</Configuration>
33
+ <Platform>x64</Platform>
34
+ </ProjectConfiguration>
35
+ </ItemGroup>
36
+
10
37
  <PropertyGroup Label="Globals">
11
38
  <CppWinRTOptimized>true</CppWinRTOptimized>
12
39
  <CppWinRTRootNamespaceAutoMerge>true</CppWinRTRootNamespaceAutoMerge>
@@ -21,12 +48,15 @@
21
48
  <ApplicationTypeRevision>10.0</ApplicationTypeRevision>
22
49
  </PropertyGroup>
23
50
 
24
- <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
25
-
26
51
  <PropertyGroup Label="ReactNativeWindowsProps">
27
52
  <ReactNativeWindowsDir Condition="'$(ReactNativeWindowsDir)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\</ReactNativeWindowsDir>
28
53
  </PropertyGroup>
29
54
 
55
+ <!-- 导入 RNW Windows SDK 默认配置(设置 WindowsTargetPlatformVersion) -->
56
+ <Import Project="$(ReactNativeWindowsDir)\PropertySheets\External\Microsoft.ReactNative.WindowsSdk.Default.props" Condition="Exists('$(ReactNativeWindowsDir)\PropertySheets\External\Microsoft.ReactNative.WindowsSdk.Default.props')" />
57
+
58
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
59
+
30
60
  <PropertyGroup Label="Configuration">
31
61
  <ConfigurationType>DynamicLibrary</ConfigurationType>
32
62
  <PlatformToolset>v143</PlatformToolset>
@@ -51,6 +81,11 @@
51
81
  <Import Project="PropertySheet.props" />
52
82
  </ImportGroup>
53
83
 
84
+ <!-- 导入 RNW UWP C++ Lib 配置(设置 TargetFrameworkMoniker、AssetTargetFallback、MIDL 引用等) -->
85
+ <ImportGroup Label="ReactNativeWindowsPropertySheets" Condition="'$(ReactNativeWindowsDir)' != ''">
86
+ <Import Project="$(ReactNativeWindowsDir)\PropertySheets\External\Microsoft.ReactNative.CppLib.props" />
87
+ </ImportGroup>
88
+
54
89
  <PropertyGroup Label="UserMacros" />
55
90
 
56
91
  <ItemDefinitionGroup>
@@ -66,6 +101,7 @@
66
101
  <AdditionalIncludeDirectories>$(ReactNativeWindowsDir);$(ReactNativeWindowsDir)Microsoft.ReactNative;$(ReactNativeWindowsDir)Microsoft.ReactNative.Cxx;$(ReactNativeWindowsDir)include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
67
102
  <LanguageStandard>stdcpp20</LanguageStandard>
68
103
  <PreprocessorDefinitions>NOMINMAX;WIN32_LEAN_AND_MEAN;SECURITY_WIN32;%(PreprocessorDefinitions)</PreprocessorDefinitions>
104
+ <AdditionalOptions>%(AdditionalOptions) /GS-</AdditionalOptions>
69
105
  </ClCompile>
70
106
  <Link>
71
107
  <SubSystem>Console</SubSystem>
@@ -88,6 +124,7 @@
88
124
  <ClCompile Include="SmtpProtocol.cpp" />
89
125
  <ClCompile Include="TlsSocket.cpp" />
90
126
  <ClCompile Include="ReactPackageProvider.cpp" />
127
+ <ClCompile Include="Generated Files\module.g.cpp" />
91
128
  <ClCompile Include="pch.cpp">
92
129
  <PrecompiledHeader>Create</PrecompiledHeader>
93
130
  </ClCompile>
@@ -109,22 +146,12 @@
109
146
  <Midl Include="ReactPackageProvider.idl" />
110
147
  </ItemGroup>
111
148
 
112
- <!-- ═══════════════════════════════════════════════════════ -->
113
- <!-- NuGet 引用 -->
114
- <!-- ═══════════════════════════════════════════════════════ -->
115
-
116
- <ItemGroup>
117
- <PackageReference Include="Microsoft.ReactNative.WindowsSdk">
118
- <Version>10.0.18362.0</Version>
119
- </PackageReference>
120
- <PackageReference Include="Microsoft.Windows.CppWinRT">
121
- <Version>2.0.240111.5</Version>
122
- </PackageReference>
123
- </ItemGroup>
124
-
125
149
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
126
150
 
127
- <!-- 导入 Microsoft.ReactNative UWP CppLib targets -->
151
+ <!-- 导入 RNW UWP C++ Lib targets(自动添加 Microsoft.ReactNative、Microsoft.Windows.CppWinRT 等包引用) -->
152
+ <ImportGroup Label="ReactNativeWindowsTargets" Condition="'$(ReactNativeWindowsDir)' != ''">
153
+ <Import Project="$(ReactNativeWindowsDir)\PropertySheets\External\Microsoft.ReactNative.CppLib.targets" />
154
+ </ImportGroup>
128
155
  <ImportGroup Label="ReactNativeWindowsTargets">
129
156
  <Import Project="$(ReactNativeWindowsDir)\Microsoft.ReactNative.Cxx\Microsoft.ReactNative.Cxx.targets" Condition="Exists('$(ReactNativeWindowsDir)\Microsoft.ReactNative.Cxx\Microsoft.ReactNative.Cxx.targets')" />
130
157
  <Import Project="$(ReactNativeWindowsDir)\Microsoft.ReactNative\Microsoft.ReactNative.targets" Condition="Exists('$(ReactNativeWindowsDir)\Microsoft.ReactNative\Microsoft.ReactNative.targets')" />
@@ -9,6 +9,9 @@
9
9
  #include <unordered_map>
10
10
  #include <memory>
11
11
  #include <mutex>
12
+ #include <sstream>
13
+ #include <optional>
14
+ #include <thread>
12
15
 
13
16
  #include "winrt/Microsoft.ReactNative.h"
14
17
  #include "NativeModules.h"
@@ -18,6 +21,95 @@
18
21
 
19
22
  namespace winrt::EmailImapSmtp {
20
23
 
24
+ // ─── JSON 序列化辅助 ────────────────────────────────────
25
+ namespace detail {
26
+
27
+ inline std::string escapeJson(const std::string& s) {
28
+ std::string out;
29
+ out.reserve(s.size() + 2);
30
+ for (char c : s) {
31
+ switch (c) {
32
+ case '"': out += "\\\""; break;
33
+ case '\\': out += "\\\\"; break;
34
+ case '\n': out += "\\n"; break;
35
+ case '\r': out += "\\r"; break;
36
+ case '\t': out += "\\t"; break;
37
+ default: out += c;
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+
43
+ inline std::string toJson(const emailimap::MailboxInfo& mb) {
44
+ std::ostringstream os;
45
+ os << "{"
46
+ << "\"name\":\"" << escapeJson(mb.name) << "\","
47
+ << "\"path\":\"" << escapeJson(mb.path) << "\","
48
+ << "\"uidNext\":" << mb.uidNext << ","
49
+ << "\"uidValidity\":" << mb.uidValidity << ","
50
+ << "\"messageCount\":" << mb.messageCount << ","
51
+ << "\"recentCount\":" << mb.recentCount << ","
52
+ << "\"unseenCount\":" << mb.unseenCount << ","
53
+ << "\"firstUnseenUid\":" << mb.firstUnseenUid
54
+ << "}";
55
+ return os.str();
56
+ }
57
+
58
+ inline std::string toJson(const emailimap::MailboxListItem& item) {
59
+ std::ostringstream os;
60
+ os << "{"
61
+ << "\"name\":\"" << escapeJson(item.name) << "\","
62
+ << "\"path\":\"" << escapeJson(item.path) << "\","
63
+ << "\"subscribed\":" << (item.subscribed ? "true" : "false")
64
+ << "}";
65
+ return os.str();
66
+ }
67
+
68
+ inline std::string toJson(const emailimap::EmailAddress& addr) {
69
+ std::ostringstream os;
70
+ os << "{"
71
+ << "\"name\":\"" << escapeJson(addr.name) << "\","
72
+ << "\"email\":\"" << escapeJson(addr.email) << "\""
73
+ << "}";
74
+ return os.str();
75
+ }
76
+
77
+ inline std::string toJson(const emailimap::AttachmentInfo& att) {
78
+ std::ostringstream os;
79
+ os << "{"
80
+ << "\"filename\":\"" << escapeJson(att.filename) << "\","
81
+ << "\"mimeType\":\"" << escapeJson(att.mimeType) << "\","
82
+ << "\"partId\":\"" << escapeJson(att.partId) << "\","
83
+ << "\"size\":" << att.size
84
+ << "}";
85
+ return os.str();
86
+ }
87
+
88
+ inline std::string toJson(const emailimap::EmailMessage& msg) {
89
+ std::ostringstream os;
90
+ os << "{"
91
+ << "\"uid\":" << msg.uid << ","
92
+ << "\"subject\":\"" << escapeJson(msg.subject) << "\","
93
+ << "\"from\":" << toJson(msg.from) << ","
94
+ << "\"date\":\"" << escapeJson(msg.date) << "\","
95
+ << "\"size\":" << msg.size
96
+ << "}";
97
+ return os.str();
98
+ }
99
+
100
+ template<typename T, typename Fn>
101
+ inline std::string arrayToJson(const std::vector<T>& items, Fn serializer) {
102
+ std::string result = "[";
103
+ for (size_t i = 0; i < items.size(); ++i) {
104
+ if (i > 0) result += ",";
105
+ result += serializer(items[i]);
106
+ }
107
+ result += "]";
108
+ return result;
109
+ }
110
+
111
+ } // namespace detail
112
+
21
113
  struct SessionState {
22
114
  std::unique_ptr<emailimap::ImapClient> imapClient;
23
115
  std::unique_ptr<emailimap::SmtpClient> smtpClient;
@@ -25,7 +117,7 @@ struct SessionState {
25
117
  bool smtpConnected = false;
26
118
  };
27
119
 
28
- REACT_MODULE(EmailImapSmtpModule, "EmailImapSmtp");
120
+ REACT_MODULE(EmailImapSmtpModule, L"EmailImapSmtp");
29
121
  struct EmailImapSmtpModule {
30
122
  // ─── Session 管理 ─────────────────────────────────────
31
123
  std::unordered_map<std::string, std::shared_ptr<SessionState>> m_sessions;
@@ -54,85 +146,348 @@ struct EmailImapSmtpModule {
54
146
  m_sessions.erase(id);
55
147
  }
56
148
 
149
+ // ─── 异步辅助 ─────────────────────────────────────────
150
+ // 在后台线程执行网络操作,不阻塞 JS 线程
151
+ template<typename Fn>
152
+ void runAsync(React::ReactPromise<std::string> const& promise, Fn&& fn) noexcept {
153
+ std::thread([promise, fn = std::forward<Fn>(fn)]() {
154
+ try {
155
+ fn();
156
+ } catch (const std::exception& e) {
157
+ std::string err = detail::escapeJson(e.what());
158
+ promise.Resolve("{\"success\":false,\"error\":\"" + err + "\"}");
159
+ } catch (...) {
160
+ promise.Resolve("{\"success\":false,\"error\":\"Unknown error\"}");
161
+ }
162
+ }).detach();
163
+ }
164
+
57
165
  // ─── IMAP 连接 ────────────────────────────────────────
58
166
 
59
167
  REACT_METHOD(imapConnect);
60
- std::string imapConnect(const std::string& configJson) noexcept {
61
- try {
168
+ void imapConnect(std::string configJson, React::ReactPromise<std::string> promise) noexcept {
169
+ runAsync(promise, [this, configJson = std::move(configJson), promise]() {
62
170
  auto config = emailimap::ConnectionConfig{};
63
- // TODO: parse configJson config
64
- // 使用 JSON 库(如 nlohmann/json)解析
171
+ auto extractStr = [](const std::string& json, const std::string& key) -> std::string {
172
+ std::string search = "\"" + key + "\":\"";
173
+ size_t pos = json.find(search);
174
+ if (pos == std::string::npos) return "";
175
+ pos += search.size();
176
+ size_t end = json.find("\"", pos);
177
+ if (end == std::string::npos) return "";
178
+ return json.substr(pos, end - pos);
179
+ };
180
+ auto extractDbl = [](const std::string& json, const std::string& key, double def) -> double {
181
+ std::string search = "\"" + key + "\":";
182
+ size_t pos = json.find(search);
183
+ if (pos == std::string::npos) return def;
184
+ pos += search.size();
185
+ size_t end = json.find_first_of(",}", pos);
186
+ if (end == std::string::npos) return def;
187
+ try { return std::stod(json.substr(pos, end - pos)); }
188
+ catch (...) { return def; }
189
+ };
190
+ auto extractBool = [](const std::string& json, const std::string& key, bool def) -> bool {
191
+ std::string search = "\"" + key + "\":true";
192
+ if (json.find(search) != std::string::npos) return true;
193
+ search = "\"" + key + "\":false";
194
+ if (json.find(search) != std::string::npos) return false;
195
+ return def;
196
+ };
197
+ config.host = extractStr(configJson, "host");
198
+ config.port = extractDbl(configJson, "port", 993);
199
+ config.username = extractStr(configJson, "username");
200
+ config.password = extractStr(configJson, "password");
201
+ config.tls = extractBool(configJson, "tls", true);
202
+ config.authType = extractStr(configJson, "authType");
203
+ if (config.authType.empty()) config.authType = "password";
204
+ config.timeout = extractDbl(configJson, "timeout", 30000);
205
+ config.checkCertificate = extractBool(configJson, "checkCertificate", true);
206
+ config.debug = extractBool(configJson, "debug", false);
65
207
 
66
208
  auto sessionId = createSession();
67
209
  auto session = getSession(sessionId);
68
210
  std::string error;
69
211
  if (session->imapClient->connect(config, error)) {
70
212
  session->imapConnected = true;
71
- return "{\"success\":true,\"sessionId\":\"" + sessionId + "\"}";
213
+ promise.Resolve("{\"success\":true,\"sessionId\":\"" + sessionId + "\"}");
214
+ return;
72
215
  }
73
216
  removeSession(sessionId);
74
- return "{\"success\":false,\"error\":\"" + escapeJson(error) + "\"}";
75
- } catch (const std::exception& e) {
76
- return "{\"success\":false,\"error\":\"" + escapeJson(e.what()) + "\"}";
77
- }
217
+ promise.Resolve("{\"success\":false,\"error\":\"" + detail::escapeJson(error) + "\"}");
218
+ });
78
219
  }
79
220
 
80
221
  REACT_METHOD(imapDisconnect);
81
- void imapDisconnect(const std::string& sessionId) noexcept {
82
- auto session = getSession(sessionId);
83
- if (session && session->imapConnected) {
84
- session->imapClient->disconnect();
85
- session->imapConnected = false;
86
- }
87
- removeSession(sessionId);
222
+ void imapDisconnect(std::string sessionId, React::ReactPromise<std::string> promise) noexcept {
223
+ runAsync(promise, [this, sessionId = std::move(sessionId), promise]() {
224
+ auto session = getSession(sessionId);
225
+ if (session && session->imapConnected) {
226
+ session->imapClient->disconnect();
227
+ session->imapConnected = false;
228
+ }
229
+ removeSession(sessionId);
230
+ promise.Resolve("{\"success\":true}");
231
+ });
232
+ }
233
+
234
+ // ─── IMAP 其他方法 ──────────────────────────────────
235
+
236
+ REACT_METHOD(imapListMailboxes);
237
+ void imapListMailboxes(std::string sessionId, React::ReactPromise<std::string> promise) noexcept {
238
+ runAsync(promise, [this, sessionId = std::move(sessionId), promise]() {
239
+ auto session = getSession(sessionId);
240
+ if (!session || !session->imapConnected) {
241
+ promise.Resolve("{\"success\":false,\"error\":\"Not connected\"}");
242
+ return;
243
+ }
244
+ std::string error;
245
+ auto mailboxes = session->imapClient->listMailboxes(error);
246
+ if (!error.empty()) {
247
+ promise.Resolve("{\"success\":false,\"error\":\"" + detail::escapeJson(error) + "\"}");
248
+ return;
249
+ }
250
+ promise.Resolve("{\"success\":true,\"mailboxes\":" +
251
+ detail::arrayToJson(mailboxes, [](const emailimap::MailboxListItem& i) { return detail::toJson(i); }) + "}");
252
+ });
253
+ }
254
+
255
+ REACT_METHOD(imapSelectMailbox);
256
+ void imapSelectMailbox(std::string sessionId, std::string mailbox, bool readOnly, React::ReactPromise<std::string> promise) noexcept {
257
+ runAsync(promise, [this, sessionId = std::move(sessionId), mailbox = std::move(mailbox), readOnly, promise]() {
258
+ auto session = getSession(sessionId);
259
+ if (!session || !session->imapConnected) {
260
+ promise.Resolve("{\"success\":false,\"error\":\"Not connected\"}");
261
+ return;
262
+ }
263
+ std::string error;
264
+ auto info = session->imapClient->selectMailbox(mailbox, readOnly, error);
265
+ if (!error.empty()) {
266
+ promise.Resolve("{\"success\":false,\"error\":\"" + detail::escapeJson(error) + "\"}");
267
+ return;
268
+ }
269
+ promise.Resolve("{\"success\":true,\"mailbox\":" + detail::toJson(info) + "}");
270
+ });
271
+ }
272
+
273
+ REACT_METHOD(imapFetchEmails);
274
+ void imapFetchEmails(std::string sessionId, std::string optionsJson, React::ReactPromise<std::string> promise) noexcept {
275
+ runAsync(promise, [this, sessionId = std::move(sessionId), optionsJson = std::move(optionsJson), promise]() {
276
+ auto session = getSession(sessionId);
277
+ if (!session || !session->imapConnected) {
278
+ promise.Resolve("{\"success\":false,\"error\":\"Not connected\"}");
279
+ return;
280
+ }
281
+ emailimap::FetchEmailsOptions opts;
282
+ auto extractDouble = [](const std::string& json, const std::string& key) -> std::optional<double> {
283
+ std::string search = "\"" + key + "\":";
284
+ size_t pos = json.find(search);
285
+ if (pos == std::string::npos) return std::nullopt;
286
+ pos += search.size();
287
+ size_t end = json.find_first_of(",}", pos);
288
+ if (end == std::string::npos) return std::nullopt;
289
+ std::string val = json.substr(pos, end - pos);
290
+ size_t start = val.find_first_not_of(" \t\r\n\"");
291
+ size_t finish = val.find_last_not_of(" \t\r\n\"");
292
+ if (start == std::string::npos) return std::nullopt;
293
+ val = val.substr(start, finish - start + 1);
294
+ try { return std::stod(val); }
295
+ catch (...) { return std::nullopt; }
296
+ };
297
+ auto extractString = [](const std::string& json, const std::string& key) -> std::optional<std::string> {
298
+ std::string search = "\"" + key + "\":\"";
299
+ size_t pos = json.find(search);
300
+ if (pos == std::string::npos) return std::nullopt;
301
+ pos += search.size();
302
+ size_t end = json.find("\"", pos);
303
+ if (end == std::string::npos) return std::nullopt;
304
+ return json.substr(pos, end - pos);
305
+ };
306
+ auto extractBool = [](const std::string& json, const std::string& key) -> bool {
307
+ std::string search = "\"" + key + "\":true";
308
+ return json.find(search) != std::string::npos;
309
+ };
310
+ auto page = extractDouble(optionsJson, "page");
311
+ auto pageSize = extractDouble(optionsJson, "pageSize");
312
+ auto from = extractDouble(optionsJson, "from");
313
+ auto to = extractDouble(optionsJson, "to");
314
+ if (page.has_value()) opts.page = page.value();
315
+ if (pageSize.has_value()) opts.pageSize = pageSize.value();
316
+ if (from.has_value()) opts.from = from.value();
317
+ if (to.has_value()) opts.to = to.value();
318
+ opts.fetchBody = extractBool(optionsJson, "fetchBody");
319
+ opts.fetchAttachments = extractBool(optionsJson, "fetchAttachments");
320
+ opts.fetchFlags = extractBool(optionsJson, "fetchFlags");
321
+ opts.fetchHeaders = extractBool(optionsJson, "fetchHeaders");
322
+ auto since = extractString(optionsJson, "since");
323
+ auto before = extractString(optionsJson, "before");
324
+ if (since.has_value()) opts.since = since.value();
325
+ if (before.has_value()) opts.before = before.value();
326
+ std::string error;
327
+ auto emails = session->imapClient->fetchEmails(opts, error);
328
+ if (!error.empty()) {
329
+ promise.Resolve("{\"success\":false,\"error\":\"" + detail::escapeJson(error) + "\"}");
330
+ return;
331
+ }
332
+ promise.Resolve("{\"success\":true,\"emails\":" +
333
+ detail::arrayToJson(emails, [](const emailimap::EmailMessage& e) { return detail::toJson(e); }) + "}");
334
+ });
335
+ }
336
+
337
+ REACT_METHOD(imapFetchEmailByUID);
338
+ void imapFetchEmailByUID(std::string sessionId, double uid, React::ReactPromise<std::string> promise) noexcept {
339
+ runAsync(promise, [this, sessionId = std::move(sessionId), uid, promise]() {
340
+ auto session = getSession(sessionId);
341
+ if (!session || !session->imapConnected) {
342
+ promise.Resolve("{\"success\":false,\"error\":\"Not connected\"}");
343
+ return;
344
+ }
345
+ std::string error;
346
+ auto* msg = session->imapClient->fetchEmailByUID(uid, error);
347
+ if (!msg) {
348
+ promise.Resolve("{\"success\":false,\"error\":\"" + detail::escapeJson(error) + "\"}");
349
+ return;
350
+ }
351
+ promise.Resolve("{\"success\":true,\"email\":" + detail::toJson(*msg) + "}");
352
+ delete msg;
353
+ });
354
+ }
355
+
356
+ REACT_METHOD(imapSearchEmails);
357
+ void imapSearchEmails(std::string sessionId, std::string criteriaJson, React::ReactPromise<std::string> promise) noexcept {
358
+ runAsync(promise, [this, sessionId = std::move(sessionId), criteriaJson = std::move(criteriaJson), promise]() {
359
+ auto session = getSession(sessionId);
360
+ if (!session || !session->imapConnected) {
361
+ promise.Resolve("{\"success\":false,\"error\":\"Not connected\"}");
362
+ return;
363
+ }
364
+ emailimap::SearchCriteria criteria;
365
+ auto extractStr = [](const std::string& json, const std::string& key) -> std::optional<std::string> {
366
+ std::string search = "\"" + key + "\":\"";
367
+ size_t pos = json.find(search);
368
+ if (pos == std::string::npos) return std::nullopt;
369
+ pos += search.size();
370
+ size_t end = json.find("\"", pos);
371
+ if (end == std::string::npos) return std::nullopt;
372
+ return json.substr(pos, end - pos);
373
+ };
374
+ auto extractBool = [](const std::string& json, const std::string& key) -> bool {
375
+ return json.find("\"" + key + "\":true") != std::string::npos;
376
+ };
377
+ auto subject = extractStr(criteriaJson, "subject");
378
+ auto from = extractStr(criteriaJson, "from");
379
+ auto to = extractStr(criteriaJson, "to");
380
+ auto text = extractStr(criteriaJson, "text");
381
+ auto body = extractStr(criteriaJson, "body");
382
+ auto since = extractStr(criteriaJson, "since");
383
+ auto before = extractStr(criteriaJson, "before");
384
+ if (subject.has_value()) criteria.subject = subject.value();
385
+ if (from.has_value()) criteria.from = from.value();
386
+ if (to.has_value()) criteria.to = to.value();
387
+ if (text.has_value()) criteria.text = text.value();
388
+ if (body.has_value()) criteria.body = body.value();
389
+ if (since.has_value()) criteria.since = since.value();
390
+ if (before.has_value()) criteria.before = before.value();
391
+ criteria.flagged = extractBool(criteriaJson, "flagged");
392
+ criteria.seen = extractBool(criteriaJson, "seen");
393
+ std::string error;
394
+ auto emails = session->imapClient->searchEmails(criteria, error);
395
+ if (!error.empty()) {
396
+ promise.Resolve("{\"success\":false,\"error\":\"" + detail::escapeJson(error) + "\"}");
397
+ return;
398
+ }
399
+ promise.Resolve("{\"success\":true,\"emails\":" +
400
+ detail::arrayToJson(emails, [](const emailimap::EmailMessage& e) { return detail::toJson(e); }) + "}");
401
+ });
88
402
  }
89
403
 
90
404
  // ─── SMTP ──────────────────────────────────────────────
91
405
 
92
406
  REACT_METHOD(smtpConnect);
93
- std::string smtpConnect(const std::string& configJson) noexcept {
94
- try {
407
+ void smtpConnect(std::string configJson, React::ReactPromise<std::string> promise) noexcept {
408
+ runAsync(promise, [this, configJson = std::move(configJson), promise]() {
95
409
  auto config = emailimap::SmtpConnectionConfig{};
96
- // TODO: parse configJson config
410
+ auto extractStr = [](const std::string& json, const std::string& key) -> std::string {
411
+ std::string search = "\"" + key + "\":\"";
412
+ size_t pos = json.find(search);
413
+ if (pos == std::string::npos) return "";
414
+ pos += search.size();
415
+ size_t end = json.find("\"", pos);
416
+ if (end == std::string::npos) return "";
417
+ return json.substr(pos, end - pos);
418
+ };
419
+ auto extractDbl = [](const std::string& json, const std::string& key, double def) -> double {
420
+ std::string search = "\"" + key + "\":";
421
+ size_t pos = json.find(search);
422
+ if (pos == std::string::npos) return def;
423
+ pos += search.size();
424
+ size_t end = json.find_first_of(",}", pos);
425
+ if (end == std::string::npos) return def;
426
+ try { return std::stod(json.substr(pos, end - pos)); }
427
+ catch (...) { return def; }
428
+ };
429
+ auto extractBool = [](const std::string& json, const std::string& key, bool def) -> bool {
430
+ std::string search = "\"" + key + "\":true";
431
+ if (json.find(search) != std::string::npos) return true;
432
+ search = "\"" + key + "\":false";
433
+ if (json.find(search) != std::string::npos) return false;
434
+ return def;
435
+ };
436
+ config.host = extractStr(configJson, "host");
437
+ config.port = extractDbl(configJson, "port", 465);
438
+ config.username = extractStr(configJson, "username");
439
+ config.password = extractStr(configJson, "password");
440
+ config.tls = extractBool(configJson, "tls", true);
441
+ config.authType = extractStr(configJson, "authType");
442
+ if (config.authType.empty()) config.authType = "password";
443
+ config.timeout = extractDbl(configJson, "timeout", 30000);
444
+ config.checkCertificate = extractBool(configJson, "checkCertificate", true);
445
+ config.debug = extractBool(configJson, "debug", false);
97
446
 
98
447
  auto sessionId = createSession();
99
448
  auto session = getSession(sessionId);
100
449
  std::string error;
101
450
  if (session->smtpClient->connect(config, error)) {
102
451
  session->smtpConnected = true;
103
- return "{\"success\":true,\"sessionId\":\"" + sessionId + "\"}";
452
+ promise.Resolve("{\"success\":true,\"sessionId\":\"" + sessionId + "\"}");
453
+ return;
104
454
  }
105
455
  removeSession(sessionId);
106
- return "{\"success\":false,\"error\":\"" + escapeJson(error) + "\"}";
107
- } catch (const std::exception& e) {
108
- return "{\"success\":false,\"error\":\"" + escapeJson(e.what()) + "\"}";
109
- }
456
+ promise.Resolve("{\"success\":false,\"error\":\"" + detail::escapeJson(error) + "\"}");
457
+ });
110
458
  }
111
459
 
112
460
  REACT_METHOD(smtpDisconnect);
113
- void smtpDisconnect(const std::string& sessionId) noexcept {
114
- auto session = getSession(sessionId);
115
- if (session && session->smtpConnected) {
116
- session->smtpClient->disconnect();
117
- session->smtpConnected = false;
118
- }
119
- removeSession(sessionId);
461
+ void smtpDisconnect(std::string sessionId, React::ReactPromise<std::string> promise) noexcept {
462
+ runAsync(promise, [this, sessionId = std::move(sessionId), promise]() {
463
+ auto session = getSession(sessionId);
464
+ if (session && session->smtpConnected) {
465
+ session->smtpClient->disconnect();
466
+ session->smtpConnected = false;
467
+ }
468
+ removeSession(sessionId);
469
+ promise.Resolve("{\"success\":true}");
470
+ });
120
471
  }
121
472
 
122
- private:
123
- static std::string escapeJson(const std::string& s) {
124
- std::string result;
125
- for (char c : s) {
126
- switch (c) {
127
- case '"': result += "\\\""; break;
128
- case '\\': result += "\\\\"; break;
129
- case '\n': result += "\\n"; break;
130
- case '\r': result += "\\r"; break;
131
- case '\t': result += "\\t"; break;
132
- default: result += c;
473
+ REACT_METHOD(smtpSendMail);
474
+ void smtpSendMail(std::string sessionId, std::string mailJson, React::ReactPromise<std::string> promise) noexcept {
475
+ runAsync(promise, [this, sessionId = std::move(sessionId), mailJson = std::move(mailJson), promise]() {
476
+ auto session = getSession(sessionId);
477
+ if (!session || !session->smtpConnected) {
478
+ promise.Resolve("{\"success\":false,\"error\":\"SMTP not connected\"}");
479
+ return;
133
480
  }
134
- }
135
- return result;
481
+ emailimap::SendMailOptions opts;
482
+ emailimap::SendMailResult result;
483
+ // TODO: parse mailJson → opts
484
+ std::string error;
485
+ if (session->smtpClient->sendMail(opts, result, error)) {
486
+ promise.Resolve("{\"success\":true}");
487
+ return;
488
+ }
489
+ promise.Resolve("{\"success\":false,\"error\":\"" + detail::escapeJson(error) + "\"}");
490
+ });
136
491
  }
137
492
  };
138
493
 
@@ -4,11 +4,10 @@
4
4
 
5
5
  namespace EmailImapSmtp
6
6
  {
7
- [webhosthidden]
8
7
  [default_interface]
9
8
  runtimeclass ReactPackageProvider : Microsoft.ReactNative.IReactPackageProvider
10
9
  {
11
10
  ReactPackageProvider();
12
11
  };
13
12
  }
14
- }
13
+
@@ -5,6 +5,129 @@
5
5
  #include "pch.h"
6
6
  #include "TlsSocket.h"
7
7
 
8
+ // Manual SSPI/Schannel declarations for UWP compat (SDK excludes these)
9
+ #pragma comment(lib, "ws2_32.lib")
10
+ #pragma comment(lib, "crypt32.lib")
11
+ #pragma comment(lib, "secur32.lib")
12
+
13
+ #ifndef SCHANNEL_CRED_VERSION
14
+ #define SECURITY_STATUS int
15
+ #define SEC_E_OK 0
16
+ #define SEC_I_CONTINUE_NEEDED 0x00090312L
17
+ #define SEC_E_INCOMPLETE_MESSAGE 0x80090318L
18
+ #define SECPKG_ATTR_STREAM_SIZES 0x00000004
19
+ #define SECPKG_CRED_OUTBOUND 2
20
+ #define SCHANNEL_CRED_VERSION 4
21
+ #define SP_PROT_TLS1_2 0x00000800
22
+ #define SCH_CRED_MANUAL_CRED_VALIDATION 0x00000008
23
+ #define SCH_CRED_NO_SYSTEM_MAPPER 0x00000002
24
+ #define SCH_CRED_NO_SERVERNAME_CHECK 0x00000004
25
+ #define SECBUFFER_VERSION 0
26
+ #define SECBUFFER_EMPTY 0
27
+ #define SECBUFFER_DATA 1
28
+ #define SECBUFFER_TOKEN 2
29
+ #define SECBUFFER_STREAM_HEADER 5
30
+ #define SECBUFFER_STREAM_TRAILER 6
31
+ #define SECBUFFER_EXTRA 9
32
+ #define SECBUFFER_MISSING 11
33
+ #define ISC_REQ_SEQUENCE_DETECT 0x00000008
34
+ #define ISC_REQ_REPLAY_DETECT 0x00000004
35
+ #define ISC_REQ_CONFIDENTIALITY 0x00000010
36
+ #define ISC_REQ_ALLOCATE_MEMORY 0x00000100
37
+ #define ISC_REQ_STREAM 0x00008000
38
+ #define ISC_REQ_MANUAL_CRED_VALIDATION 0x00080000
39
+
40
+ typedef struct _SecHandle {
41
+ ULONG_PTR dwLower;
42
+ ULONG_PTR dwUpper;
43
+ } SecHandle;
44
+ typedef SecHandle CredHandle;
45
+ typedef SecHandle CtxtHandle;
46
+
47
+ typedef struct _SecBuffer {
48
+ unsigned long cbBuffer;
49
+ unsigned long BufferType;
50
+ void* pvBuffer;
51
+ } SecBuffer;
52
+
53
+ typedef struct _SecBufferDesc {
54
+ unsigned long ulVersion;
55
+ unsigned long cBuffers;
56
+ SecBuffer* pBuffers;
57
+ } SecBufferDesc;
58
+
59
+ typedef struct _SCHANNEL_CRED {
60
+ DWORD dwVersion;
61
+ DWORD cCreds;
62
+ void* paCred;
63
+ void* hRootStore;
64
+ DWORD cMappers;
65
+ void* aphMappers;
66
+ DWORD cSupportedAlgs;
67
+ void* paSupportedAlgs;
68
+ DWORD dwFlags;
69
+ DWORD dwCredFormat;
70
+ DWORD grbitEnabledProtocols;
71
+ DWORD dwMinimumCipherStrength;
72
+ DWORD dwMaximumCipherStrength;
73
+ } SCHANNEL_CRED;
74
+
75
+ typedef struct _SecPkgContext_StreamSizes {
76
+ unsigned long cbHeader;
77
+ unsigned long cbTrailer;
78
+ unsigned long cbMaximumMessage;
79
+ unsigned long cBuffers;
80
+ unsigned long cbBlockSize;
81
+ unsigned long cbMaxTrailer;
82
+ } SecPkgContext_StreamSizes;
83
+
84
+ typedef struct _SecPkgContext_Sizes {
85
+ unsigned long cbMaxToken;
86
+ unsigned long cbMaxSignature;
87
+ unsigned long cbBlockSize;
88
+ unsigned long cbMaxTrailer;
89
+ } SecPkgContext_Sizes;
90
+
91
+ typedef struct _TimeStamp {
92
+ LONG LowPart;
93
+ LONG HighPart;
94
+ } TimeStamp;
95
+
96
+ #ifndef SEC_ENTRY
97
+ #define SEC_ENTRY
98
+ #endif
99
+ extern "C" {
100
+ int SEC_ENTRY AcquireCredentialsHandleW(LPWSTR, LPWSTR, unsigned long, void*, void*, void*, void*, CredHandle*, TimeStamp*);
101
+ int SEC_ENTRY AcquireCredentialsHandleA(LPSTR, LPSTR, unsigned long, void*, void*, void*, void*, CredHandle*, TimeStamp*);
102
+ int SEC_ENTRY InitializeSecurityContextA(CredHandle*, void*, LPSTR, unsigned long, unsigned long, unsigned long, SecBufferDesc*, unsigned long, CtxtHandle*, SecBufferDesc*, unsigned long*, TimeStamp*);
103
+ int SEC_ENTRY InitializeSecurityContextW(CredHandle*, void*, LPWSTR, unsigned long, unsigned long, unsigned long, SecBufferDesc*, unsigned long, CtxtHandle*, SecBufferDesc*, unsigned long*, TimeStamp*);
104
+ int SEC_ENTRY DeleteSecurityContext(CtxtHandle*);
105
+ int SEC_ENTRY FreeCredentialsHandle(CredHandle*);
106
+ int SEC_ENTRY FreeContextBuffer(void*);
107
+ int SEC_ENTRY EncryptMessage(CtxtHandle*, unsigned long, SecBufferDesc*, unsigned long);
108
+ int SEC_ENTRY DecryptMessage(CtxtHandle*, SecBufferDesc*, unsigned long, void*);
109
+ int SEC_ENTRY QueryContextAttributesW(CtxtHandle*, unsigned long, void*);
110
+ int SEC_ENTRY QueryContextAttributesA(CtxtHandle*, unsigned long, void*);
111
+ }
112
+
113
+ #define UNISP_NAME_A "Microsoft Unified Security Protocol Provider"
114
+ #define UNISP_NAME UNISP_NAME_A
115
+ #define AcquireCredentialsHandle AcquireCredentialsHandleA
116
+ #define QueryContextAttributes QueryContextAttributesA
117
+
118
+ #endif // SCHANNEL_CRED_VERSION
119
+
120
+ // Cast helpers: uint8_t[] members -> typed references for SSPI API calls
121
+ #ifdef SCHANNEL_CRED_VERSION
122
+ #define credHandle (*reinterpret_cast<CredHandle*>(m_credHandle))
123
+ #define ctxtHandle (*reinterpret_cast<CtxtHandle*>(m_ctxtHandle))
124
+ #define streamSizes (*reinterpret_cast<SecPkgContext_StreamSizes*>(m_streamSizes))
125
+ #else
126
+ #define credHandle m_credHandle
127
+ #define ctxtHandle m_ctxtHandle
128
+ #define streamSizes m_streamSizes
129
+ #endif
130
+
8
131
  #include <sstream>
9
132
  #include <cstring>
10
133
  #include <chrono>
@@ -250,11 +373,11 @@ bool TlsSocket::connect(const std::string& host, int port, int timeoutMs, bool c
250
373
 
251
374
  void TlsSocket::disconnectInternal() {
252
375
  if (m_hasCtxtHandle) {
253
- DeleteSecurityContext(&m_ctxtHandle);
376
+ DeleteSecurityContext(&ctxtHandle);
254
377
  m_hasCtxtHandle = false;
255
378
  }
256
379
  if (m_hasCredHandle) {
257
- FreeCredentialsHandle(&m_credHandle);
380
+ FreeCredentialsHandle(&credHandle);
258
381
  m_hasCredHandle = false;
259
382
  }
260
383
  if (m_socket != INVALID_SOCKET) {
@@ -299,7 +422,7 @@ bool TlsSocket::initializeSchannel() {
299
422
  &schannelCred,
300
423
  nullptr,
301
424
  nullptr,
302
- &m_credHandle,
425
+ &credHandle,
303
426
  &expiry
304
427
  );
305
428
 
@@ -338,7 +461,7 @@ bool TlsSocket::performClientHandshake() {
338
461
  TimeStamp expiry;
339
462
 
340
463
  status = InitializeSecurityContextW(
341
- &m_credHandle,
464
+ &credHandle,
342
465
  nullptr,
343
466
  const_cast<wchar_t*>(wServerName.c_str()),
344
467
  flags,
@@ -346,7 +469,7 @@ bool TlsSocket::performClientHandshake() {
346
469
  0,
347
470
  nullptr,
348
471
  0,
349
- &m_ctxtHandle,
472
+ &ctxtHandle,
350
473
  &outDesc,
351
474
  &outFlags,
352
475
  &expiry
@@ -405,8 +528,8 @@ bool TlsSocket::performClientHandshake() {
405
528
  outBuffers[0].BufferType = SECBUFFER_TOKEN;
406
529
 
407
530
  status = InitializeSecurityContextW(
408
- &m_credHandle,
409
- &m_ctxtHandle,
531
+ &credHandle,
532
+ &ctxtHandle,
410
533
  const_cast<wchar_t*>(wServerName.c_str()),
411
534
  flags,
412
535
  0,
@@ -478,7 +601,7 @@ bool TlsSocket::performClientHandshake() {
478
601
  }
479
602
 
480
603
  if (status == SEC_E_OK) {
481
- QueryContextAttributes(&m_ctxtHandle, SECPKG_ATTR_STREAM_SIZES, &m_streamSizes);
604
+ QueryContextAttributes(&ctxtHandle, SECPKG_ATTR_STREAM_SIZES, &streamSizes);
482
605
  m_hasCtxtHandle = true;
483
606
  // Save any extra (unconsumed) data to m_receiveBuffer so the caller
484
607
  // can read it (e.g. IMAP greeting piggybacked on the handshake).
@@ -506,22 +629,22 @@ bool TlsSocket::performClientHandshake() {
506
629
  bool TlsSocket::encryptMessage(const std::string& plaintext, std::vector<char>& ciphertext) {
507
630
  if (!m_hasCtxtHandle) return false;
508
631
 
509
- int needed = m_streamSizes.cbHeader + static_cast<int>(plaintext.size()) + m_streamSizes.cbTrailer;
632
+ int needed = streamSizes.cbHeader + static_cast<int>(plaintext.size()) + streamSizes.cbTrailer;
510
633
  std::vector<char> msg(needed);
511
634
 
512
635
  SecBuffer buffers[4];
513
- buffers[0].cbBuffer = m_streamSizes.cbHeader;
636
+ buffers[0].cbBuffer = streamSizes.cbHeader;
514
637
  buffers[0].BufferType = SECBUFFER_STREAM_HEADER;
515
638
  buffers[0].pvBuffer = msg.data();
516
639
 
517
640
  buffers[1].cbBuffer = static_cast<unsigned long>(plaintext.size());
518
641
  buffers[1].BufferType = SECBUFFER_DATA;
519
- buffers[1].pvBuffer = msg.data() + m_streamSizes.cbHeader;
642
+ buffers[1].pvBuffer = msg.data() + streamSizes.cbHeader;
520
643
  memcpy(buffers[1].pvBuffer, plaintext.data(), plaintext.size());
521
644
 
522
- buffers[2].cbBuffer = m_streamSizes.cbTrailer;
645
+ buffers[2].cbBuffer = streamSizes.cbTrailer;
523
646
  buffers[2].BufferType = SECBUFFER_STREAM_TRAILER;
524
- buffers[2].pvBuffer = msg.data() + m_streamSizes.cbHeader + plaintext.size();
647
+ buffers[2].pvBuffer = msg.data() + streamSizes.cbHeader + plaintext.size();
525
648
 
526
649
  buffers[3].cbBuffer = 0;
527
650
  buffers[3].BufferType = SECBUFFER_EMPTY;
@@ -531,7 +654,7 @@ bool TlsSocket::encryptMessage(const std::string& plaintext, std::vector<char>&
531
654
  desc.cBuffers = 4;
532
655
  desc.pBuffers = buffers;
533
656
 
534
- SECURITY_STATUS status = EncryptMessage(&m_ctxtHandle, 0, &desc, 0);
657
+ SECURITY_STATUS status = EncryptMessage(&ctxtHandle, 0, &desc, 0);
535
658
  if (status != SEC_E_OK) return false;
536
659
 
537
660
  int totalSize = buffers[0].cbBuffer + buffers[1].cbBuffer + buffers[2].cbBuffer;
@@ -564,7 +687,7 @@ int TlsSocket::decryptMessage(std::vector<char>& buffer, std::vector<char>& plai
564
687
  desc.cBuffers = 4;
565
688
  desc.pBuffers = buffers;
566
689
 
567
- SECURITY_STATUS status = DecryptMessage(&m_ctxtHandle, &desc, 0, nullptr);
690
+ SECURITY_STATUS status = DecryptMessage(&ctxtHandle, &desc, 0, nullptr);
568
691
  if (status == SEC_E_INCOMPLETE_MESSAGE) {
569
692
  // need more data
570
693
  return 0; // need more data
@@ -58,13 +58,13 @@ public:
58
58
  private:
59
59
  SOCKET m_socket = INVALID_SOCKET;
60
60
  bool m_tls = false;
61
- CredHandle m_credHandle{};
62
- CtxtHandle m_ctxtHandle{};
61
+ uint8_t m_credHandle[16];
62
+ uint8_t m_ctxtHandle[16];
63
63
  bool m_hasCredHandle = false;
64
64
  bool m_hasCtxtHandle = false;
65
65
  std::string m_serverName;
66
66
  bool m_checkCertificate = true;
67
- SecPkgContext_StreamSizes m_streamSizes{};
67
+ uint8_t m_streamSizes[24];
68
68
 
69
69
  std::mutex m_mutex;
70
70
  int m_lastError = 0; // stores WSAGetLastError() on failure
@@ -6,7 +6,9 @@
6
6
  <!-- 不使用 WinUI 3 -->
7
7
  <UseWinUI3>false</UseWinUI3>
8
8
  <!-- 不使用实验性 NuGet -->
9
- <UseExperimentalNuget>false</UseExperimentalNuget>
9
+ <UseExperimentalNuget>true</UseExperimentalNuget>
10
+ <RnwNewArch>true</RnwNewArch>
11
+ <RunCodegenWindows>false</RunCodegenWindows>
10
12
  <!-- 不使用 Folly (不再需要) -->
11
13
  <UseFolly>false</UseFolly>
12
14
  </PropertyGroup>