react-native-email-imap-smtp 0.1.0

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 (201) hide show
  1. package/EmailImapSmtp.podspec +29 -0
  2. package/LICENSE +20 -0
  3. package/README.md +121 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +117 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +11 -0
  8. package/android/src/main/java/com/margelo/nitro/emailimapsmtp/EmailImapSmtp.kt +1533 -0
  9. package/android/src/main/java/com/margelo/nitro/emailimapsmtp/EmailImapSmtpPackage.kt +22 -0
  10. package/ios/EmailImapSmtp.swift +5 -0
  11. package/lib/module/EmailImapSmtp.nitro.js +4 -0
  12. package/lib/module/EmailImapSmtp.nitro.js.map +1 -0
  13. package/lib/module/NativeEmailImapSmtp.js +14 -0
  14. package/lib/module/NativeEmailImapSmtp.js.map +1 -0
  15. package/lib/module/index.js +64 -0
  16. package/lib/module/index.js.map +1 -0
  17. package/lib/module/native.js +227 -0
  18. package/lib/module/native.js.map +1 -0
  19. package/lib/module/package.json +1 -0
  20. package/lib/module/types.js +2 -0
  21. package/lib/module/types.js.map +1 -0
  22. package/lib/module/web.js +107 -0
  23. package/lib/module/web.js.map +1 -0
  24. package/lib/module/windows.js +156 -0
  25. package/lib/module/windows.js.map +1 -0
  26. package/lib/typescript/package.json +1 -0
  27. package/lib/typescript/src/EmailImapSmtp.nitro.d.ts +268 -0
  28. package/lib/typescript/src/EmailImapSmtp.nitro.d.ts.map +1 -0
  29. package/lib/typescript/src/NativeEmailImapSmtp.d.ts +36 -0
  30. package/lib/typescript/src/NativeEmailImapSmtp.d.ts.map +1 -0
  31. package/lib/typescript/src/index.d.ts +33 -0
  32. package/lib/typescript/src/index.d.ts.map +1 -0
  33. package/lib/typescript/src/native.d.ts +32 -0
  34. package/lib/typescript/src/native.d.ts.map +1 -0
  35. package/lib/typescript/src/types.d.ts +261 -0
  36. package/lib/typescript/src/types.d.ts.map +1 -0
  37. package/lib/typescript/src/web.d.ts +32 -0
  38. package/lib/typescript/src/web.d.ts.map +1 -0
  39. package/lib/typescript/src/windows.d.ts +32 -0
  40. package/lib/typescript/src/windows.d.ts.map +1 -0
  41. package/nitro.json +23 -0
  42. package/nitrogen/generated/android/c++/JAppendResult.hpp +65 -0
  43. package/nitrogen/generated/android/c++/JAttachmentInfo.hpp +96 -0
  44. package/nitrogen/generated/android/c++/JConnectionConfig.hpp +94 -0
  45. package/nitrogen/generated/android/c++/JConnectionResult.hpp +73 -0
  46. package/nitrogen/generated/android/c++/JDeleteResult.hpp +87 -0
  47. package/nitrogen/generated/android/c++/JEmailAddress.hpp +61 -0
  48. package/nitrogen/generated/android/c++/JEmailBody.hpp +84 -0
  49. package/nitrogen/generated/android/c++/JEmailMessage.hpp +273 -0
  50. package/nitrogen/generated/android/c++/JFetchEmailsOptions.hpp +110 -0
  51. package/nitrogen/generated/android/c++/JFolderInfo.hpp +126 -0
  52. package/nitrogen/generated/android/c++/JFunc_void_IdleNotification.hpp +79 -0
  53. package/nitrogen/generated/android/c++/JHybridEmailImapSmtpSpec.cpp +697 -0
  54. package/nitrogen/generated/android/c++/JHybridEmailImapSmtpSpec.hpp +92 -0
  55. package/nitrogen/generated/android/c++/JIdleNotification.hpp +78 -0
  56. package/nitrogen/generated/android/c++/JImapServerInfo.hpp +84 -0
  57. package/nitrogen/generated/android/c++/JMailboxInfo.hpp +134 -0
  58. package/nitrogen/generated/android/c++/JMailboxListItem.hpp +92 -0
  59. package/nitrogen/generated/android/c++/JQuotaInfo.hpp +78 -0
  60. package/nitrogen/generated/android/c++/JSearchCriteria.hpp +187 -0
  61. package/nitrogen/generated/android/c++/JSearchHeader.hpp +61 -0
  62. package/nitrogen/generated/android/c++/JSendMailOptions.hpp +229 -0
  63. package/nitrogen/generated/android/c++/JSendMailResult.hpp +88 -0
  64. package/nitrogen/generated/android/c++/JSmtpAttachment.hpp +80 -0
  65. package/nitrogen/generated/android/c++/JSmtpConnectionConfig.hpp +98 -0
  66. package/nitrogen/generated/android/c++/JSmtpServerInfo.hpp +102 -0
  67. package/nitrogen/generated/android/c++/JVariant_ImapServerInfo_SmtpServerInfo.cpp +26 -0
  68. package/nitrogen/generated/android/c++/JVariant_ImapServerInfo_SmtpServerInfo.hpp +73 -0
  69. package/nitrogen/generated/android/c++/JVariant_NullType_AttachmentInfo.cpp +26 -0
  70. package/nitrogen/generated/android/c++/JVariant_NullType_AttachmentInfo.hpp +75 -0
  71. package/nitrogen/generated/android/c++/JVariant_NullType_EmailMessage.cpp +26 -0
  72. package/nitrogen/generated/android/c++/JVariant_NullType_EmailMessage.hpp +83 -0
  73. package/nitrogen/generated/android/emailimapsmtp+autolinking.cmake +84 -0
  74. package/nitrogen/generated/android/emailimapsmtp+autolinking.gradle +27 -0
  75. package/nitrogen/generated/android/emailimapsmtpOnLoad.cpp +56 -0
  76. package/nitrogen/generated/android/emailimapsmtpOnLoad.hpp +34 -0
  77. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/AppendResult.kt +61 -0
  78. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/AttachmentInfo.kt +96 -0
  79. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/ConnectionConfig.kt +96 -0
  80. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/ConnectionResult.kt +61 -0
  81. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/DeleteResult.kt +61 -0
  82. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/EmailAddress.kt +56 -0
  83. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/EmailBody.kt +61 -0
  84. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/EmailMessage.kt +156 -0
  85. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/FetchEmailsOptions.kt +116 -0
  86. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/FolderInfo.kt +91 -0
  87. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/Func_void_IdleNotification.kt +80 -0
  88. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/HybridEmailImapSmtpSpec.kt +178 -0
  89. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/IdleNotification.kt +76 -0
  90. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/ImapServerInfo.kt +61 -0
  91. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/MailboxInfo.kt +101 -0
  92. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/MailboxListItem.kt +71 -0
  93. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/QuotaInfo.kt +76 -0
  94. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/SearchCriteria.kt +196 -0
  95. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/SearchHeader.kt +56 -0
  96. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/SendMailOptions.kt +126 -0
  97. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/SendMailResult.kt +86 -0
  98. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/SmtpAttachment.kt +76 -0
  99. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/SmtpConnectionConfig.kt +101 -0
  100. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/SmtpServerInfo.kt +61 -0
  101. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/Variant_ImapServerInfo_SmtpServerInfo.kt +62 -0
  102. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/Variant_NullType_AttachmentInfo.kt +62 -0
  103. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/Variant_NullType_EmailMessage.kt +62 -0
  104. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/emailimapsmtpOnLoad.kt +35 -0
  105. package/nitrogen/generated/ios/EmailImapSmtp+autolinking.rb +62 -0
  106. package/nitrogen/generated/ios/EmailImapSmtp-Swift-Cxx-Bridge.cpp +161 -0
  107. package/nitrogen/generated/ios/EmailImapSmtp-Swift-Cxx-Bridge.hpp +1132 -0
  108. package/nitrogen/generated/ios/EmailImapSmtp-Swift-Cxx-Umbrella.hpp +118 -0
  109. package/nitrogen/generated/ios/EmailImapSmtpAutolinking.mm +33 -0
  110. package/nitrogen/generated/ios/EmailImapSmtpAutolinking.swift +26 -0
  111. package/nitrogen/generated/ios/c++/HybridEmailImapSmtpSpecSwift.cpp +11 -0
  112. package/nitrogen/generated/ios/c++/HybridEmailImapSmtpSpecSwift.hpp +390 -0
  113. package/nitrogen/generated/ios/swift/AppendResult.swift +39 -0
  114. package/nitrogen/generated/ios/swift/AttachmentInfo.swift +113 -0
  115. package/nitrogen/generated/ios/swift/ConnectionConfig.swift +87 -0
  116. package/nitrogen/generated/ios/swift/ConnectionResult.swift +84 -0
  117. package/nitrogen/generated/ios/swift/DeleteResult.swift +64 -0
  118. package/nitrogen/generated/ios/swift/EmailAddress.swift +34 -0
  119. package/nitrogen/generated/ios/swift/EmailBody.swift +45 -0
  120. package/nitrogen/generated/ios/swift/EmailMessage.swift +184 -0
  121. package/nitrogen/generated/ios/swift/FetchEmailsOptions.swift +198 -0
  122. package/nitrogen/generated/ios/swift/FolderInfo.swift +81 -0
  123. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  124. package/nitrogen/generated/ios/swift/Func_void_AppendResult.swift +46 -0
  125. package/nitrogen/generated/ios/swift/Func_void_ConnectionResult.swift +46 -0
  126. package/nitrogen/generated/ios/swift/Func_void_DeleteResult.swift +46 -0
  127. package/nitrogen/generated/ios/swift/Func_void_IdleNotification.swift +46 -0
  128. package/nitrogen/generated/ios/swift/Func_void_MailboxInfo.swift +46 -0
  129. package/nitrogen/generated/ios/swift/Func_void_QuotaInfo.swift +46 -0
  130. package/nitrogen/generated/ios/swift/Func_void_SendMailResult.swift +46 -0
  131. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  132. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  133. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__AttachmentInfo_.swift +58 -0
  134. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__EmailMessage_.swift +58 -0
  135. package/nitrogen/generated/ios/swift/Func_void_std__vector_AttachmentInfo_.swift +46 -0
  136. package/nitrogen/generated/ios/swift/Func_void_std__vector_EmailMessage_.swift +46 -0
  137. package/nitrogen/generated/ios/swift/Func_void_std__vector_FolderInfo_.swift +46 -0
  138. package/nitrogen/generated/ios/swift/Func_void_std__vector_MailboxListItem_.swift +46 -0
  139. package/nitrogen/generated/ios/swift/HybridEmailImapSmtpSpec.swift +84 -0
  140. package/nitrogen/generated/ios/swift/HybridEmailImapSmtpSpec_cxx.swift +759 -0
  141. package/nitrogen/generated/ios/swift/IdleNotification.swift +93 -0
  142. package/nitrogen/generated/ios/swift/ImapServerInfo.swift +45 -0
  143. package/nitrogen/generated/ios/swift/MailboxInfo.swift +91 -0
  144. package/nitrogen/generated/ios/swift/MailboxListItem.swift +55 -0
  145. package/nitrogen/generated/ios/swift/QuotaInfo.swift +80 -0
  146. package/nitrogen/generated/ios/swift/SearchCriteria.swift +485 -0
  147. package/nitrogen/generated/ios/swift/SearchHeader.swift +34 -0
  148. package/nitrogen/generated/ios/swift/SendMailOptions.swift +310 -0
  149. package/nitrogen/generated/ios/swift/SendMailResult.swift +155 -0
  150. package/nitrogen/generated/ios/swift/SmtpAttachment.swift +80 -0
  151. package/nitrogen/generated/ios/swift/SmtpConnectionConfig.swift +105 -0
  152. package/nitrogen/generated/ios/swift/SmtpServerInfo.swift +51 -0
  153. package/nitrogen/generated/ios/swift/Variant_ImapServerInfo_SmtpServerInfo.swift +30 -0
  154. package/nitrogen/generated/ios/swift/Variant_NullType_AttachmentInfo.swift +30 -0
  155. package/nitrogen/generated/ios/swift/Variant_NullType_EmailMessage.swift +30 -0
  156. package/nitrogen/generated/shared/c++/AppendResult.hpp +91 -0
  157. package/nitrogen/generated/shared/c++/AttachmentInfo.hpp +121 -0
  158. package/nitrogen/generated/shared/c++/ConnectionConfig.hpp +120 -0
  159. package/nitrogen/generated/shared/c++/ConnectionResult.hpp +98 -0
  160. package/nitrogen/generated/shared/c++/DeleteResult.hpp +93 -0
  161. package/nitrogen/generated/shared/c++/EmailAddress.hpp +87 -0
  162. package/nitrogen/generated/shared/c++/EmailBody.hpp +92 -0
  163. package/nitrogen/generated/shared/c++/EmailMessage.hpp +177 -0
  164. package/nitrogen/generated/shared/c++/FetchEmailsOptions.hpp +136 -0
  165. package/nitrogen/generated/shared/c++/FolderInfo.hpp +116 -0
  166. package/nitrogen/generated/shared/c++/HybridEmailImapSmtpSpec.cpp +50 -0
  167. package/nitrogen/generated/shared/c++/HybridEmailImapSmtpSpec.hpp +145 -0
  168. package/nitrogen/generated/shared/c++/IdleNotification.hpp +104 -0
  169. package/nitrogen/generated/shared/c++/ImapServerInfo.hpp +92 -0
  170. package/nitrogen/generated/shared/c++/MailboxInfo.hpp +124 -0
  171. package/nitrogen/generated/shared/c++/MailboxListItem.hpp +100 -0
  172. package/nitrogen/generated/shared/c++/QuotaInfo.hpp +104 -0
  173. package/nitrogen/generated/shared/c++/SearchCriteria.hpp +203 -0
  174. package/nitrogen/generated/shared/c++/SearchHeader.hpp +87 -0
  175. package/nitrogen/generated/shared/c++/SendMailOptions.hpp +151 -0
  176. package/nitrogen/generated/shared/c++/SendMailResult.hpp +113 -0
  177. package/nitrogen/generated/shared/c++/SmtpAttachment.hpp +105 -0
  178. package/nitrogen/generated/shared/c++/SmtpConnectionConfig.hpp +124 -0
  179. package/nitrogen/generated/shared/c++/SmtpServerInfo.hpp +92 -0
  180. package/package.json +196 -0
  181. package/react-native.config.js +27 -0
  182. package/src/EmailImapSmtp.nitro.ts +333 -0
  183. package/src/NativeEmailImapSmtp.ts +100 -0
  184. package/src/index.tsx +166 -0
  185. package/src/native.ts +305 -0
  186. package/src/types.ts +328 -0
  187. package/src/web.ts +196 -0
  188. package/src/windows.ts +304 -0
  189. package/windows/EmailImapSmtp.cpp +735 -0
  190. package/windows/EmailImapSmtp.h +137 -0
  191. package/windows/EmailImapSmtpModule.cpp +10 -0
  192. package/windows/EmailImapSmtpModule.h +139 -0
  193. package/windows/EmailImapSmtpPackage.cpp +15 -0
  194. package/windows/EmailImapSmtpPackage.h +13 -0
  195. package/windows/ImapProtocol.cpp +1382 -0
  196. package/windows/ImapProtocol.h +97 -0
  197. package/windows/ProtocolDefs.h +300 -0
  198. package/windows/SmtpProtocol.cpp +334 -0
  199. package/windows/SmtpProtocol.h +36 -0
  200. package/windows/TlsSocket.cpp +818 -0
  201. package/windows/TlsSocket.h +87 -0
@@ -0,0 +1,1382 @@
1
+ // ============================================================
2
+ // ImapProtocol.cpp — 纯 IMAP 协议实现
3
+ // 从 EmailImapSmtp.cpp 拆分,移除所有 Nitro/JSI 依赖
4
+ // ============================================================
5
+
6
+ #include "ImapProtocol.h"
7
+
8
+ #include <sstream>
9
+ #include <cstring>
10
+ #include <chrono>
11
+ #include <random>
12
+ #include <array>
13
+ #include <thread>
14
+ #include <ctime>
15
+ #include <iomanip>
16
+
17
+ namespace emailimap {
18
+
19
+ // ═══════════════════════════════════════════════════════════
20
+ // IMAP modified UTF-7 decoder (RFC 3501)
21
+ // ═══════════════════════════════════════════════════════════
22
+
23
+ static int utf16ToUtf8(uint16_t u16, char* out) {
24
+ if (u16 < 0x80) { out[0] = (char)u16; return 1; }
25
+ if (u16 < 0x800) { out[0] = (char)(0xC0 | (u16 >> 6)); out[1] = (char)(0x80 | (u16 & 0x3F)); return 2; }
26
+ out[0] = (char)(0xE0 | (u16 >> 12));
27
+ out[1] = (char)(0x80 | ((u16 >> 6) & 0x3F));
28
+ out[2] = (char)(0x80 | (u16 & 0x3F));
29
+ return 3;
30
+ }
31
+
32
+ static std::string decodeImapUtf7(const std::string& s) {
33
+ std::string result;
34
+ size_t i = 0;
35
+ while (i < s.size()) {
36
+ if (s[i] == '&') {
37
+ size_t end = s.find('-', i + 1);
38
+ if (end == std::string::npos) { result += s[i]; i++; continue; }
39
+ if (end == i + 1) {
40
+ // &- → literal &
41
+ result += '&';
42
+ i = end + 1;
43
+ continue;
44
+ }
45
+ // &base64- → decode
46
+ std::string b64 = s.substr(i + 1, end - i - 1);
47
+ // IMAP UTF-7 uses ',' instead of '/' in base64
48
+ for (char& c : b64) if (c == ',') c = '/';
49
+ std::vector<char> raw = decodeBase64(b64);
50
+ // raw bytes are UTF-16BE
51
+ for (size_t j = 0; j + 1 < raw.size(); j += 2) {
52
+ uint16_t u16 = (uint16_t)(unsigned char)raw[j] << 8 | (unsigned char)raw[j+1];
53
+ // Check for surrogate pair (U+D800-U+DBFF = high, U+DC00-U+DFFF = low)
54
+ if (u16 >= 0xD800 && u16 <= 0xDBFF && j + 3 < raw.size()) {
55
+ uint16_t lo = (uint16_t)(unsigned char)raw[j+2] << 8 | (unsigned char)raw[j+3];
56
+ if (lo >= 0xDC00 && lo <= 0xDFFF) {
57
+ uint32_t cp = 0x10000 + (u16 - 0xD800) * 0x400 + (lo - 0xDC00);
58
+ char buf[4];
59
+ buf[0] = (char)(0xF0 | (cp >> 18));
60
+ buf[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
61
+ buf[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
62
+ buf[3] = (char)(0x80 | (cp & 0x3F));
63
+ result.append(buf, 4);
64
+ j += 2;
65
+ continue;
66
+ }
67
+ }
68
+ char buf[4];
69
+ int n = utf16ToUtf8(u16, buf);
70
+ result.append(buf, n);
71
+ }
72
+ i = end + 1;
73
+ continue;
74
+ }
75
+ // Printable ASCII (except &, which is handled above)
76
+ result += s[i];
77
+ i++;
78
+ }
79
+ return result;
80
+ }
81
+
82
+ // ═══════════════════════════════════════════════════════════
83
+ // MIME Parsing helpers
84
+ // (Utility functions trim/toUpper/split/currentTimestamp/formatDate
85
+ // are defined in TlsSocket.cpp - shared by ImapProtocol and SmtpProtocol)
86
+ // ═══════════════════════════════════════════════════════════
87
+
88
+ std::string ImapClient::decodeMimeHeader(const std::string& header) {
89
+ std::string result = header;
90
+ std::regex pattern("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?=");
91
+ std::smatch match;
92
+
93
+ while (std::regex_search(result, match, pattern)) {
94
+ std::string charset = match[1];
95
+ std::string encoding = toUpper(match[2].str());
96
+ std::string encoded = match[3];
97
+
98
+ std::string decoded;
99
+ if (encoding == "B") {
100
+ auto bytes = decodeBase64(encoded);
101
+ decoded = std::string(bytes.begin(), bytes.end());
102
+ } else if (encoding == "Q") {
103
+ for (size_t i = 0; i < encoded.size(); i++) {
104
+ if (encoded[i] == '_') {
105
+ decoded += ' ';
106
+ } else if (encoded[i] == '=' && i + 2 < encoded.size()) {
107
+ std::string hex = encoded.substr(i + 1, 2);
108
+ decoded += static_cast<char>(std::stoi(hex, nullptr, 16));
109
+ i += 2;
110
+ } else {
111
+ decoded += encoded[i];
112
+ }
113
+ }
114
+ } else {
115
+ decoded = encoded;
116
+ }
117
+
118
+ result.replace(match.position(), match.length(), decoded);
119
+ }
120
+
121
+ return result;
122
+ }
123
+
124
+ std::string ImapClient::decodeQuotedPrintable(const std::string& text) {
125
+ std::string result;
126
+ for (size_t i = 0; i < text.size(); i++) {
127
+ if (text[i] == '=' && i + 2 < text.size()) {
128
+ if (text[i + 1] == '\r' && text[i + 2] == '\n') {
129
+ i += 2;
130
+ continue;
131
+ }
132
+ if (text[i + 1] == '\n') {
133
+ i += 1;
134
+ continue;
135
+ }
136
+ std::string hex = text.substr(i + 1, 2);
137
+ result += static_cast<char>(std::stoi(hex, nullptr, 16));
138
+ i += 2;
139
+ } else {
140
+ result += text[i];
141
+ }
142
+ }
143
+ return result;
144
+ }
145
+
146
+ void ImapClient::parseMimeParts(const std::string& contentType, const std::string& body,
147
+ std::string& textBody, std::string& htmlBody,
148
+ std::vector<AttachmentInfo>& attachments) {
149
+ std::string ct = toUpper(contentType);
150
+
151
+ if (ct.find("TEXT/PLAIN") != std::string::npos) {
152
+ textBody = body;
153
+ } else if (ct.find("TEXT/HTML") != std::string::npos) {
154
+ htmlBody = body;
155
+ } else if (ct.find("MULTIPART/ALTERNATIVE") != std::string::npos ||
156
+ ct.find("MULTIPART/MIXED") != std::string::npos ||
157
+ ct.find("MULTIPART/RELATED") != std::string::npos) {
158
+ std::string boundary;
159
+ std::regex boundaryRegex(R"re(boundary\s*=\s*"?([^";\s]+)"?)re", std::regex::icase);
160
+ std::smatch match;
161
+ if (std::regex_search(contentType, match, boundaryRegex) && match.size() > 1) {
162
+ boundary = match[1];
163
+ } else {
164
+ textBody = body;
165
+ return;
166
+ }
167
+
168
+ std::string delimiter = "--" + boundary;
169
+ std::vector<std::string> parts;
170
+ size_t pos = 0;
171
+ while (true) {
172
+ size_t found = body.find(delimiter, pos);
173
+ if (found == std::string::npos) break;
174
+ size_t next = body.find("\n", found);
175
+ if (next == std::string::npos) break;
176
+ size_t partEnd = body.find(delimiter, next + 1);
177
+ if (partEnd == std::string::npos) {
178
+ parts.push_back(body.substr(next + 1));
179
+ break;
180
+ }
181
+ parts.push_back(body.substr(next + 1, partEnd - next - 1));
182
+ pos = partEnd;
183
+ }
184
+
185
+ for (const auto& part : parts) {
186
+ size_t headerEnd = part.find("\r\n\r\n");
187
+ if (headerEnd == std::string::npos) {
188
+ headerEnd = part.find("\n\n");
189
+ }
190
+ if (headerEnd == std::string::npos) continue;
191
+
192
+ std::string partHeaders = part.substr(0, headerEnd);
193
+ std::string partBody = part.substr(headerEnd + 2);
194
+
195
+ std::string partCt = "text/plain";
196
+ std::regex ctRegex(R"x(Content-Type:\s*(.+?)(?:\r?\n|$))x", std::regex::icase);
197
+ std::smatch ctMatch;
198
+ if (std::regex_search(partHeaders, ctMatch, ctRegex) && ctMatch.size() > 1) {
199
+ partCt = trim(ctMatch[1]);
200
+ }
201
+ parseMimeParts(partCt, partBody, textBody, htmlBody, attachments);
202
+ }
203
+ } else {
204
+ // Attachment or unknown content type
205
+ AttachmentInfo info;
206
+ info.filename = "attachment";
207
+ info.mimeType = contentType;
208
+ info.partId = "1";
209
+ info.size = static_cast<double>(body.size());
210
+ info.data.assign(body.begin(), body.end()); // ArrayBuffer::copy → vector<uint8_t>
211
+ info.contentId = std::nullopt;
212
+ info.isInline = false;
213
+ info.charset = std::nullopt;
214
+ info.contentDisposition = "attachment";
215
+ info.localPath = std::nullopt;
216
+ attachments.push_back(info);
217
+ }
218
+ }
219
+
220
+ EmailMessage ImapClient::parseMessage(const std::string& rawHeaders, const std::string& rawBody,
221
+ double uid, double seqNum) {
222
+ EmailMessage msg;
223
+ msg.uid = uid;
224
+ msg.sequenceNumber = seqNum;
225
+ msg.subject = "";
226
+ msg.charset = "UTF-8";
227
+ msg.mimeType = "text/plain";
228
+ msg.priority = "normal";
229
+ msg.size = static_cast<double>(rawHeaders.size() + rawBody.size());
230
+ msg.messageId = "";
231
+ msg.inReplyTo = "";
232
+ msg.references = "";
233
+ msg.receivedDate = "";
234
+ msg.isEncrypted = false;
235
+
236
+ std::string textBody, htmlBody;
237
+ std::vector<AttachmentInfo> attachments;
238
+ std::string contentType = "text/plain";
239
+
240
+ std::istringstream headerStream(rawHeaders);
241
+ std::string line;
242
+ std::string currentHeader;
243
+ std::string currentValue;
244
+
245
+ auto flushHeader = [&]() {
246
+ if (currentHeader.empty()) return;
247
+ std::string headerName = toUpper(trim(currentHeader));
248
+ std::string headerValue = decodeMimeHeader(trim(currentValue));
249
+
250
+ if (headerName == "SUBJECT") {
251
+ msg.subject = headerValue;
252
+ } else if (headerName == "FROM") {
253
+ std::regex fromRegex(R"x((?:"?([^"]*)"?\s*<([^>]+)>)|(.+))x");
254
+ std::smatch m;
255
+ if (std::regex_match(headerValue, m, fromRegex)) {
256
+ EmailAddress addr;
257
+ if (m[1].matched) {
258
+ addr.name = m[1];
259
+ addr.email = m[2];
260
+ } else {
261
+ addr.email = headerValue;
262
+ }
263
+ msg.from = addr;
264
+ } else {
265
+ EmailAddress addr;
266
+ addr.email = headerValue;
267
+ msg.from = addr;
268
+ }
269
+ } else if (headerName == "TO") {
270
+ std::regex addrRegex(R"x((?:"?([^"]*)"?\s*<([^>]+)>)|([^,]+))x");
271
+ auto begin = std::sregex_iterator(headerValue.begin(), headerValue.end(), addrRegex);
272
+ auto end = std::sregex_iterator();
273
+ for (auto it = begin; it != end; ++it) {
274
+ EmailAddress addr;
275
+ if ((*it)[1].matched) {
276
+ addr.name = (*it)[1];
277
+ addr.email = (*it)[2];
278
+ } else {
279
+ addr.email = trim((*it)[0]);
280
+ }
281
+ msg.to.push_back(addr);
282
+ }
283
+ } else if (headerName == "CC") {
284
+ std::regex addrRegex(R"x((?:"?([^"]*)"?\s*<([^>]+)>)|([^,]+))x");
285
+ auto begin = std::sregex_iterator(headerValue.begin(), headerValue.end(), addrRegex);
286
+ auto end = std::sregex_iterator();
287
+ for (auto it = begin; it != end; ++it) {
288
+ EmailAddress addr;
289
+ if ((*it)[1].matched) {
290
+ addr.name = (*it)[1];
291
+ addr.email = (*it)[2];
292
+ } else {
293
+ addr.email = trim((*it)[0]);
294
+ }
295
+ msg.cc.push_back(addr);
296
+ }
297
+ } else if (headerName == "BCC") {
298
+ std::regex addrRegex(R"x((?:"?([^"]*)"?\s*<([^>]+)>)|([^,]+))x");
299
+ auto begin = std::sregex_iterator(headerValue.begin(), headerValue.end(), addrRegex);
300
+ auto end = std::sregex_iterator();
301
+ for (auto it = begin; it != end; ++it) {
302
+ EmailAddress addr;
303
+ if ((*it)[1].matched) {
304
+ addr.name = (*it)[1];
305
+ addr.email = (*it)[2];
306
+ } else {
307
+ addr.email = trim((*it)[0]);
308
+ }
309
+ msg.bcc.push_back(addr);
310
+ }
311
+ } else if (headerName == "REPLY-TO" || headerName == "REPLYTO") {
312
+ EmailAddress addr;
313
+ std::regex addrRegex(R"x((?:"?([^"]*)"?\s*<([^>]+)>)|([^,]+))x");
314
+ std::smatch m;
315
+ if (std::regex_match(headerValue, m, addrRegex)) {
316
+ if (m[1].matched) {
317
+ addr.name = m[1];
318
+ addr.email = m[2];
319
+ } else {
320
+ addr.email = trim(m[0]);
321
+ }
322
+ }
323
+ msg.replyTo.push_back(addr);
324
+ } else if (headerName == "DATE") {
325
+ msg.date = headerValue;
326
+ } else if (headerName == "MESSAGE-ID" || headerName == "MESSAGEID") {
327
+ msg.messageId = headerValue;
328
+ } else if (headerName == "IN-REPLY-TO") {
329
+ msg.inReplyTo = headerValue;
330
+ } else if (headerName == "REFERENCES") {
331
+ msg.references = headerValue;
332
+ } else if (headerName == "CONTENT-TYPE") {
333
+ contentType = headerValue;
334
+ msg.mimeType = headerValue;
335
+ } else if (headerName == "CONTENT-TRANSFER-ENCODING") {
336
+ // Handle transfer encoding
337
+ } else if (headerName == "PRIORITY" || headerName == "X-PRIORITY") {
338
+ if (headerValue.find("1") != std::string::npos || headerValue.find("2") != std::string::npos ||
339
+ toUpper(headerValue).find("HIGH") != std::string::npos || toUpper(headerValue).find("URGENT") != std::string::npos) {
340
+ msg.priority = "high";
341
+ } else if (headerValue.find("4") != std::string::npos || headerValue.find("5") != std::string::npos ||
342
+ toUpper(headerValue).find("LOW") != std::string::npos || toUpper(headerValue).find("NON-URGENT") != std::string::npos) {
343
+ msg.priority = "low";
344
+ }
345
+ } else if (headerName.find("X-") == 0) {
346
+ msg.headers[headerName] = headerValue;
347
+ } else {
348
+ msg.headers[headerName] = headerValue;
349
+ }
350
+ };
351
+
352
+ // Parse headers line by line, handling folded headers
353
+ while (std::getline(headerStream, line)) {
354
+ if (line.empty() || line == "\r") break;
355
+ if (!line.empty() && (line[0] == ' ' || line[0] == '\t')) {
356
+ currentValue += " " + trim(line);
357
+ } else {
358
+ if (!currentHeader.empty()) flushHeader();
359
+ size_t colonPos = line.find(':');
360
+ if (colonPos != std::string::npos) {
361
+ currentHeader = line.substr(0, colonPos);
362
+ currentValue = line.substr(colonPos + 1);
363
+ }
364
+ }
365
+ }
366
+ if (!currentHeader.empty()) flushHeader();
367
+
368
+ std::string bodyContent = rawBody;
369
+ parseMimeParts(contentType, bodyContent, textBody, htmlBody, attachments);
370
+
371
+ EmailBody emailBody;
372
+ emailBody.text = textBody;
373
+ emailBody.html = htmlBody;
374
+ msg.body = emailBody;
375
+ msg.attachments = attachments;
376
+
377
+ return msg;
378
+ }
379
+
380
+ // ═══════════════════════════════════════════════════════════
381
+ // IMAP Client Implementation
382
+ // ═══════════════════════════════════════════════════════════
383
+
384
+ ImapClient::ImapClient()
385
+ : m_socket(std::make_unique<TlsSocket>())
386
+ {
387
+ m_state.connected = false;
388
+ m_state.selected = false;
389
+ m_state.tagCounter = 0;
390
+ m_state.tagPrefix = "A";
391
+ }
392
+
393
+ ImapClient::~ImapClient() {
394
+ disconnect();
395
+ }
396
+
397
+ std::string ImapClient::nextTag() {
398
+ m_state.tagCounter++;
399
+ return m_state.tagPrefix + std::to_string(m_state.tagCounter);
400
+ }
401
+
402
+ bool ImapClient::sendRaw(const std::string& data) {
403
+ return m_socket->send(data);
404
+ }
405
+
406
+ std::string ImapClient::receiveLine() {
407
+ return m_socket->receiveLine(30000);
408
+ }
409
+
410
+ std::string ImapClient::receiveUntil(const std::string& delimiter) {
411
+ std::string result;
412
+ while (true) {
413
+ std::string line = receiveLine();
414
+ if (line.empty()) break;
415
+ result += line + "\r\n";
416
+ if (line == delimiter) break;
417
+ }
418
+ return result;
419
+ }
420
+
421
+ bool ImapClient::waitForTaggedResponse(const std::string& tag, std::string& response, std::string& error) {
422
+ while (true) {
423
+ std::string line = receiveLine();
424
+ if (line.empty()) {
425
+ error = "Connection closed by server";
426
+ return false;
427
+ }
428
+
429
+ if (line.size() > 0 && line[0] == '*') {
430
+ // 检测服务器主动关闭(* BYE),匹配 Android 行为
431
+ if (line.find("* BYE") == 0 || line.find("*bye") == 0) {
432
+ m_state.selected = false;
433
+ m_state.connected = false;
434
+ error = "Server closed connection";
435
+ return false;
436
+ }
437
+ continue;
438
+ }
439
+
440
+ if (line.find(tag) == 0) {
441
+ response = line;
442
+ std::string rest = line.substr(tag.size() + 1);
443
+ if (rest.find("OK") == 0) {
444
+ return true;
445
+ } else if (rest.find("NO") == 0) {
446
+ error = rest.substr(3);
447
+ return false;
448
+ } else if (rest.find("BAD") == 0) {
449
+ error = rest.substr(4);
450
+ return false;
451
+ } else {
452
+ error = "Unexpected response: " + line;
453
+ return false;
454
+ }
455
+ }
456
+ }
457
+ }
458
+
459
+ bool ImapClient::waitForTaggedResponses(const std::string& tag, std::vector<std::string>& responses, std::string& error) {
460
+ while (true) {
461
+ std::string line = receiveLine();
462
+ if (line.empty()) {
463
+ error = "Connection closed by server";
464
+ return false;
465
+ }
466
+
467
+ // 检测服务器主动关闭(* BYE),匹配 Android 行为
468
+ if (line.find("* BYE") == 0 || line.find("*bye") == 0) {
469
+ m_state.selected = false;
470
+ m_state.connected = false;
471
+ error = "Server closed connection";
472
+ return false;
473
+ }
474
+
475
+ if (line.find(tag) == 0) {
476
+ std::string rest = line.substr(tag.size() + 1);
477
+ if (rest.find("OK") == 0) {
478
+ return true;
479
+ } else {
480
+ error = rest;
481
+ return false;
482
+ }
483
+ }
484
+
485
+ responses.push_back(line);
486
+ }
487
+ }
488
+
489
+ bool ImapClient::sendCommand(const std::string& command, std::string& response, std::string& error) {
490
+ std::string tag = nextTag();
491
+ std::string cmd = tag + " " + command + "\r\n";
492
+ if (!m_socket->send(cmd)) {
493
+ error = "Failed to send command";
494
+ return false;
495
+ }
496
+ return waitForTaggedResponse(tag, response, error);
497
+ }
498
+
499
+ bool ImapClient::sendCommandMulti(const std::string& command, std::vector<std::string>& responses, std::string& error) {
500
+ std::string tag = nextTag();
501
+ std::string cmd = tag + " " + command + "\r\n";
502
+ if (!m_socket->send(cmd)) {
503
+ error = "Failed to send command";
504
+ return false;
505
+ }
506
+ return waitForTaggedResponses(tag, responses, error);
507
+ }
508
+
509
+ bool ImapClient::connect(const ConnectionConfig& config, std::string& error) {
510
+ if (!m_socket->connect(config.host, static_cast<int>(config.port),
511
+ static_cast<int>(config.timeout), config.checkCertificate)) {
512
+ error = "Failed to connect to " + config.host + ":" + std::to_string(static_cast<int>(config.port));
513
+ return false;
514
+ }
515
+
516
+ std::string greeting = receiveLine();
517
+ if (greeting.empty()) {
518
+ error = "No greeting from server";
519
+ disconnect();
520
+ return false;
521
+ }
522
+
523
+ // If StartTLS is needed but we connected without TLS
524
+ if (!config.tls && config.port != 993) {
525
+ std::string response;
526
+ if (sendCommand("STARTTLS", response, error)) {
527
+ m_socket->disconnect();
528
+ if (!m_socket->connect(config.host, static_cast<int>(config.port),
529
+ static_cast<int>(config.timeout), config.checkCertificate)) {
530
+ error = "Failed to establish TLS connection";
531
+ return false;
532
+ }
533
+ greeting = receiveLine();
534
+ if (greeting.empty()) {
535
+ error = "No greeting after STARTTLS";
536
+ disconnect();
537
+ return false;
538
+ }
539
+ }
540
+ }
541
+
542
+ // Login
543
+ bool loginSuccess = false;
544
+ if (toUpper(config.authType) == "OAUTH2" && config.accessToken.has_value()) {
545
+ std::string authString = "user=" + config.username + "\x01" +
546
+ "auth=Bearer " + config.accessToken.value() + "\x01\x01";
547
+ std::string encoded = encodeBase64Text(authString);
548
+ std::string response;
549
+ if (sendCommand("AUTHENTICATE XOAUTH2 " + encoded, response, error)) {
550
+ loginSuccess = true;
551
+ }
552
+ } else {
553
+ // Standard LOGIN with quoted strings (RFC 3501)
554
+ auto escape = [](const std::string& s) -> std::string {
555
+ std::string result = "\"";
556
+ for (char c : s) {
557
+ if (c == '\\' || c == '"') result += '\\';
558
+ result += c;
559
+ }
560
+ result += '"';
561
+ return result;
562
+ };
563
+ std::string response;
564
+ if (sendCommand("LOGIN " + escape(config.username) + " " + escape(config.password), response, error)) {
565
+ loginSuccess = true;
566
+ }
567
+ }
568
+
569
+ if (!loginSuccess) {
570
+ if (error.empty()) error = "Login failed";
571
+ disconnect();
572
+ return false;
573
+ }
574
+
575
+ m_state.connected = true;
576
+ m_state.config = config;
577
+
578
+ // Send ID command (RFC 2971) - many Chinese email servers require this
579
+ std::string idCmd = "ID (\"name\" \"react-native-email-imap-smtp\""
580
+ " \"version\" \"1.0.0\""
581
+ " \"vendor\" \"react-native-email-imap-smtp\""
582
+ " \"os\" \"Windows\""
583
+ " \"os-version\" \"10\")";
584
+ std::string idResponse;
585
+ sendCommand(idCmd, idResponse, error); // Ignore failure
586
+
587
+ return true;
588
+ }
589
+
590
+ void ImapClient::disconnect() {
591
+ if (m_state.connected) {
592
+ std::string error;
593
+ sendCommand("LOGOUT", error, error);
594
+ }
595
+ m_socket->disconnect();
596
+ m_state.connected = false;
597
+ m_state.selected = false;
598
+ m_state.selectedMailbox.clear();
599
+ }
600
+
601
+ bool ImapClient::isConnected() const {
602
+ return m_state.connected && m_socket->isConnected();
603
+ }
604
+
605
+ // ─── Mailbox operations ─────────────────────────────────────
606
+
607
+ std::vector<MailboxListItem> ImapClient::listMailboxes(std::string& error) {
608
+ std::vector<MailboxListItem> result;
609
+ std::vector<std::string> responses;
610
+
611
+ if (!sendCommandMulti("LIST \"\" \"*\"", responses, error)) {
612
+ return result;
613
+ }
614
+
615
+ std::vector<std::string> subscribed;
616
+ sendCommandMulti("LSUB \"\" \"*\"", subscribed, error);
617
+ std::set<std::string> subscribedPaths;
618
+ for (const auto& line : subscribed) {
619
+ std::regex pathRegex(R"re(LSUB\s+.*\s+"?([^"]+)"?\s*$)re");
620
+ std::smatch m;
621
+ if (std::regex_search(line, m, pathRegex) && m.size() > 1) {
622
+ subscribedPaths.insert(trim(m[1]));
623
+ }
624
+ }
625
+
626
+ for (const auto& line : responses) {
627
+ std::regex listRegex(R"re(\*\s+LIST\s+\(([^)]*)\)\s+"([^"]*)"\s+"?([^"]+)"?")re");
628
+ std::smatch match;
629
+ if (std::regex_search(line, match, listRegex) && match.size() > 3) {
630
+ MailboxListItem item;
631
+ item.name = decodeImapUtf7(match[3]);
632
+ item.path = item.name;
633
+ item.delimiter = decodeImapUtf7(match[2]);
634
+ if (match[1].matched) {
635
+ std::string flagsStr = match[1].str();
636
+ std::istringstream flagStream(flagsStr);
637
+ std::string flag;
638
+ while (flagStream >> flag) {
639
+ item.flags.push_back(flag);
640
+ }
641
+ }
642
+ item.subscribed = subscribedPaths.find(item.path) != subscribedPaths.end();
643
+ result.push_back(item);
644
+ }
645
+ }
646
+
647
+ return result;
648
+ }
649
+
650
+ MailboxInfo ImapClient::selectMailbox(const std::string& mailbox, bool readOnly, std::string& error) {
651
+ MailboxInfo info;
652
+ info.name = mailbox;
653
+ info.path = mailbox;
654
+
655
+ std::string cmd = readOnly ? "EXAMINE " : "SELECT ";
656
+ if (mailbox.find(' ') != std::string::npos || mailbox.find('"') != std::string::npos) {
657
+ std::string escaped;
658
+ for (char c : mailbox) {
659
+ if (c == '\\' || c == '"') escaped += '\\';
660
+ escaped += c;
661
+ }
662
+ cmd += "\"" + escaped + "\"";
663
+ } else {
664
+ cmd += mailbox;
665
+ }
666
+
667
+ std::vector<std::string> responses;
668
+ if (!sendCommandMulti(cmd, responses, error)) {
669
+ return info;
670
+ }
671
+
672
+ for (const auto& line : responses) {
673
+ std::regex existsRegex(R"x(\*\s+(\d+)\s+EXISTS)x");
674
+ std::smatch m;
675
+ if (std::regex_search(line, m, existsRegex) && m.size() > 1) {
676
+ info.messageCount = std::stod(m[1]);
677
+ }
678
+
679
+ std::regex recentRegex(R"x(\*\s+(\d+)\s+RECENT)x");
680
+ if (std::regex_search(line, m, recentRegex) && m.size() > 1) {
681
+ info.recentCount = std::stod(m[1]);
682
+ }
683
+
684
+ std::regex uidNextRegex(R"x(\[UIDNEXT\s+(\d+)\])x");
685
+ if (std::regex_search(line, m, uidNextRegex) && m.size() > 1) {
686
+ info.uidNext = std::stod(m[1]);
687
+ }
688
+
689
+ std::regex uidValRegex(R"x(\[UIDVALIDITY\s+(\d+)\])x");
690
+ if (std::regex_search(line, m, uidValRegex) && m.size() > 1) {
691
+ info.uidValidity = std::stod(m[1]);
692
+ }
693
+
694
+ std::regex unseenRegex(R"x(\[UNSEEN\s+(\d+)\])x");
695
+ if (std::regex_search(line, m, unseenRegex) && m.size() > 1) {
696
+ info.unseenCount = std::stod(m[1]);
697
+ }
698
+
699
+ std::regex modSeqRegex(R"x(\[HIGHESTMODSEQ\s+(\d+)\])x");
700
+ if (std::regex_search(line, m, modSeqRegex) && m.size() > 1) {
701
+ info.highestModSeq = std::stod(m[1]);
702
+ }
703
+
704
+ std::regex permFlagsRegex(R"x(\[PERMANENTFLAGS\s+\(([^)]*)\)\])x");
705
+ if (std::regex_search(line, m, permFlagsRegex) && m.size() > 1) {
706
+ std::string flagsStr = m[1];
707
+ std::istringstream flagStream(flagsStr);
708
+ std::string flag;
709
+ while (flagStream >> flag) {
710
+ info.permanentFlags.push_back(flag);
711
+ }
712
+ }
713
+
714
+ std::regex flagsRegex(R"x(\[FLAGS\s+\(([^)]*)\)\])x");
715
+ if (std::regex_search(line, m, flagsRegex) && m.size() > 1) {
716
+ std::string flagsStr = m[1];
717
+ std::istringstream flagStream(flagsStr);
718
+ std::string flag;
719
+ while (flagStream >> flag) {
720
+ info.flags.push_back(flag);
721
+ }
722
+ }
723
+ }
724
+
725
+ m_state.selected = true;
726
+ m_state.selectedMailbox = mailbox;
727
+
728
+ return info;
729
+ }
730
+
731
+ MailboxInfo ImapClient::refreshMailbox(std::string& error) {
732
+ if (!m_state.selected) {
733
+ error = "No mailbox selected";
734
+ return MailboxInfo();
735
+ }
736
+ std::string response;
737
+ sendCommand("NOOP", response, error);
738
+ return selectMailbox(m_state.selectedMailbox, false, error);
739
+ }
740
+
741
+ std::vector<FolderInfo> ImapClient::fetchFolders(std::string& error) {
742
+ std::vector<FolderInfo> result;
743
+ std::vector<std::string> responses;
744
+
745
+ if (!sendCommandMulti("LIST \"\" \"*\"", responses, error)) {
746
+ return result;
747
+ }
748
+
749
+ for (const auto& line : responses) {
750
+ std::regex listRegex(R"re(\*\s+LIST\s+\(([^)]*)\)\s+"([^"]*)"\s+"?([^"]+)"?")re");
751
+ std::smatch match;
752
+ if (std::regex_search(line, match, listRegex) && match.size() > 3) {
753
+ FolderInfo folder;
754
+ folder.name = decodeImapUtf7(match[3]);
755
+ folder.path = folder.name;
756
+ folder.delimiter = decodeImapUtf7(match[2]);
757
+ folder.parent = "";
758
+ folder.subscribed = false;
759
+ if (match[1].matched) {
760
+ std::string flagsStr = match[1].str();
761
+ std::istringstream flagStream(flagsStr);
762
+ std::string flag;
763
+ while (flagStream >> flag) {
764
+ folder.flags.push_back(flag);
765
+ }
766
+ }
767
+ result.push_back(folder);
768
+ }
769
+ }
770
+
771
+ return result;
772
+ }
773
+
774
+ bool ImapClient::createFolder(const std::string& folderName, std::string& error) {
775
+ std::string response;
776
+ return sendCommand("CREATE \"" + folderName + "\"", response, error);
777
+ }
778
+
779
+ bool ImapClient::deleteFolder(const std::string& folderName, std::string& error) {
780
+ std::string response;
781
+ return sendCommand("DELETE \"" + folderName + "\"", response, error);
782
+ }
783
+
784
+ bool ImapClient::renameFolder(const std::string& oldName, const std::string& newName, std::string& error) {
785
+ std::string response;
786
+ return sendCommand("RENAME \"" + oldName + "\" \"" + newName + "\"", response, error);
787
+ }
788
+
789
+ bool ImapClient::subscribeFolder(const std::string& folderName, std::string& error) {
790
+ std::string response;
791
+ return sendCommand("SUBSCRIBE \"" + folderName + "\"", response, error);
792
+ }
793
+
794
+ bool ImapClient::unsubscribeFolder(const std::string& folderName, std::string& error) {
795
+ std::string response;
796
+ return sendCommand("UNSUBSCRIBE \"" + folderName + "\"", response, error);
797
+ }
798
+
799
+ // ─── Fetch Emails ───────────────────────────────────────────
800
+
801
+ std::vector<EmailMessage> ImapClient::fetchEmails(const FetchEmailsOptions& options, std::string& error) {
802
+ std::vector<EmailMessage> result;
803
+
804
+ if (options.mailbox.has_value() && options.mailbox.value() != m_state.selectedMailbox) {
805
+ selectMailbox(options.mailbox.value(), false, error);
806
+ }
807
+
808
+ if (!m_state.selected) {
809
+ // 匹配 Android 行为:FolderClosed 时重选邮箱重试一次
810
+ if (!m_state.selectedMailbox.empty() && (!options.mailbox.has_value() || options.mailbox.value() == m_state.selectedMailbox)) {
811
+ selectMailbox(m_state.selectedMailbox, false, error);
812
+ if (m_state.selected) {
813
+ return fetchEmails(options, error);
814
+ }
815
+ }
816
+ error = "No mailbox selected";
817
+ return result;
818
+ }
819
+
820
+ // ── 构建 UID 列表(支持 since/before 日期过滤,匹配 Android 行为)──
821
+ std::vector<long> targetUids;
822
+ bool hasDateFilter = options.since.has_value() || options.before.has_value();
823
+
824
+ // 先通过 UID SEARCH 获取匹配日期范围的 UID 列表
825
+ if (hasDateFilter) {
826
+ std::string searchCmd = "UID SEARCH ALL";
827
+ if (options.since.has_value()) searchCmd += " SINCE " + options.since.value();
828
+ if (options.before.has_value()) searchCmd += " BEFORE " + options.before.value();
829
+
830
+ std::vector<std::string> searchResp;
831
+ if (!sendCommandMulti(searchCmd, searchResp, error)) {
832
+ // 匹配 Android 行为:FolderClosed 时重选邮箱重试一次
833
+ if (!m_state.selected && !m_state.selectedMailbox.empty()) {
834
+ selectMailbox(m_state.selectedMailbox, false, error);
835
+ if (m_state.selected) return fetchEmails(options, error);
836
+ }
837
+ return result;
838
+ }
839
+ for (const auto& line : searchResp) {
840
+ std::regex uidRegex(R"x(\*\s+SEARCH\s+(.+))x");
841
+ std::smatch m;
842
+ if (std::regex_match(line, m, uidRegex) && m.size() > 1) {
843
+ std::istringstream uidStream(m[1].str());
844
+ long uid;
845
+ while (uidStream >> uid) {
846
+ targetUids.push_back(uid);
847
+ }
848
+ }
849
+ }
850
+ }
851
+
852
+ // 根据范围/分页模式筛选 UID
853
+ if (options.from.has_value() && options.to.has_value() && options.rangeType.has_value() &&
854
+ toUpper(options.rangeType.value()) == "UID") {
855
+ // UID 范围模式
856
+ long fromUid = static_cast<long>(options.from.value());
857
+ long toUid = static_cast<long>(options.to.value());
858
+ if (!hasDateFilter) {
859
+ for (long u = fromUid; u <= toUid; u++) targetUids.push_back(u);
860
+ } else {
861
+ // 与日期过滤取交集
862
+ std::vector<long> filtered;
863
+ for (long u : targetUids) {
864
+ if (u >= fromUid && u <= toUid) filtered.push_back(u);
865
+ }
866
+ targetUids = filtered;
867
+ }
868
+ } else if (options.from.has_value() && options.to.has_value()) {
869
+ // 序列号范围模式:与日期过滤组合
870
+ std::string searchCmd = "UID SEARCH " +
871
+ std::to_string(static_cast<long>(options.from.value())) + ":" +
872
+ std::to_string(static_cast<long>(options.to.value()));
873
+ if (options.since.has_value()) searchCmd += " SINCE " + options.since.value();
874
+ if (options.before.has_value()) searchCmd += " BEFORE " + options.before.value();
875
+ std::vector<std::string> searchResp;
876
+ if (!sendCommandMulti(searchCmd, searchResp, error)) {
877
+ // 匹配 Android 行为:FolderClosed 时重选邮箱重试一次
878
+ if (!m_state.selected && !m_state.selectedMailbox.empty()) {
879
+ selectMailbox(m_state.selectedMailbox, false, error);
880
+ if (m_state.selected) return fetchEmails(options, error);
881
+ }
882
+ return result;
883
+ }
884
+ for (const auto& line : searchResp) {
885
+ std::regex uidRegex(R"x(\*\s+SEARCH\s+(.+))x");
886
+ std::smatch m;
887
+ if (std::regex_match(line, m, uidRegex) && m.size() > 1) {
888
+ std::istringstream uidStream(m[1].str());
889
+ long uid;
890
+ while (uidStream >> uid) {
891
+ targetUids.push_back(uid);
892
+ }
893
+ }
894
+ }
895
+ } else if (options.page.has_value() || !hasDateFilter) {
896
+ // 分页模式或默认模式
897
+ if (!hasDateFilter) {
898
+ std::vector<std::string> searchResp;
899
+ sendCommandMulti("UID SEARCH ALL", searchResp, error);
900
+ for (const auto& line : searchResp) {
901
+ std::regex uidRegex(R"x(\*\s+SEARCH\s+(.+))x");
902
+ std::smatch m;
903
+ if (std::regex_match(line, m, uidRegex) && m.size() > 1) {
904
+ std::istringstream uidStream(m[1].str());
905
+ long uid;
906
+ while (uidStream >> uid) {
907
+ targetUids.push_back(uid);
908
+ }
909
+ }
910
+ }
911
+ }
912
+ }
913
+
914
+ // 按 UID 降序排列(最新的在前)
915
+ std::sort(targetUids.begin(), targetUids.end(), std::greater<long>());
916
+
917
+ // 分页截取
918
+ if (options.page.has_value()) {
919
+ int page = static_cast<int>(options.page.value());
920
+ int pageSize = static_cast<int>(options.pageSize.value_or(20));
921
+ int startIdx = (page - 1) * pageSize;
922
+ int endIdx = std::min(startIdx + pageSize, static_cast<int>(targetUids.size()));
923
+ if (startIdx < static_cast<int>(targetUids.size())) {
924
+ targetUids = std::vector<long>(targetUids.begin() + startIdx, targetUids.begin() + endIdx);
925
+ } else {
926
+ return result; // 页码超出范围
927
+ }
928
+ } else if (!hasDateFilter && !options.from.has_value() && !options.to.has_value()) {
929
+ // 默认只取最近 20 封(匹配 Android 行为)
930
+ if (targetUids.size() > 20) targetUids.resize(20);
931
+ }
932
+
933
+ if (targetUids.empty()) return result;
934
+
935
+ // 逐条获取完整邮件内容(匹配 Android 行为,与 searchEmails 同一模式)
936
+ for (long uid : targetUids) {
937
+ std::string fetchError;
938
+ EmailMessage* msg = fetchEmailByUID(static_cast<double>(uid), fetchError);
939
+ if (msg) {
940
+ result.push_back(*msg);
941
+ delete msg;
942
+ }
943
+ }
944
+
945
+ // 匹配 Android 行为:FolderClosed 时重选邮箱重试一次
946
+ if (result.empty() && !m_state.selected && !m_state.selectedMailbox.empty()) {
947
+ selectMailbox(m_state.selectedMailbox, false, error);
948
+ if (m_state.selected) {
949
+ return fetchEmails(options, error);
950
+ }
951
+ }
952
+
953
+ return result;
954
+ }
955
+
956
+ EmailMessage* ImapClient::fetchEmailByUID(double uid, std::string& error) {
957
+ if (!m_state.selected) {
958
+ error = "No mailbox selected";
959
+ return nullptr;
960
+ }
961
+
962
+ std::vector<std::string> responses;
963
+ std::string cmd = "UID FETCH " + std::to_string(static_cast<long>(uid)) +
964
+ " (FLAGS INTERNALDATE BODY.PEEK[HEADER] BODY.PEEK[TEXT])";
965
+
966
+ if (!sendCommandMulti(cmd, responses, error)) {
967
+ return nullptr;
968
+ }
969
+
970
+ std::string headers, body;
971
+ bool inHeader = false, inText = false;
972
+
973
+ for (const auto& line : responses) {
974
+ if (line.find("BODY[HEADER]") != std::string::npos) {
975
+ inHeader = true; inText = false; continue;
976
+ }
977
+ if (line.find("BODY[TEXT]") != std::string::npos) {
978
+ inHeader = false; inText = true; continue;
979
+ }
980
+ if (line == ")") {
981
+ inHeader = false; inText = false; continue;
982
+ }
983
+ if (inHeader) headers += line + "\r\n";
984
+ if (inText) body += line + "\r\n";
985
+ }
986
+
987
+ if (headers.empty() && body.empty()) return nullptr;
988
+
989
+ EmailMessage msg = parseMessage(headers, body, uid, 0);
990
+ return new EmailMessage(std::move(msg));
991
+ }
992
+
993
+ // ─── Search ─────────────────────────────────────────────────
994
+
995
+ std::vector<EmailMessage> ImapClient::searchEmails(const SearchCriteria& criteria, std::string& error) {
996
+ std::vector<EmailMessage> result;
997
+
998
+ if (criteria.mailbox.has_value() && criteria.mailbox.value() != m_state.selectedMailbox) {
999
+ selectMailbox(criteria.mailbox.value(), false, error);
1000
+ }
1001
+
1002
+ if (!m_state.selected) {
1003
+ error = "No mailbox selected";
1004
+ return result;
1005
+ }
1006
+
1007
+ if (criteria.uids.has_value() && !criteria.uids.value().empty()) {
1008
+ for (double uid : criteria.uids.value()) {
1009
+ EmailMessage* msg = fetchEmailByUID(uid, error);
1010
+ if (msg) {
1011
+ result.push_back(*msg);
1012
+ delete msg;
1013
+ }
1014
+ }
1015
+ return result;
1016
+ }
1017
+
1018
+ std::string searchCmd = "UID SEARCH ALL";
1019
+
1020
+ if (criteria.subject.has_value()) searchCmd += " SUBJECT \"" + criteria.subject.value() + "\"";
1021
+ if (criteria.from.has_value()) searchCmd += " FROM \"" + criteria.from.value() + "\"";
1022
+ if (criteria.to.has_value()) searchCmd += " TO \"" + criteria.to.value() + "\"";
1023
+ if (criteria.body.has_value()) searchCmd += " BODY \"" + criteria.body.value() + "\"";
1024
+ if (criteria.text.has_value()) searchCmd += " TEXT \"" + criteria.text.value() + "\"";
1025
+ if (criteria.since.has_value()) searchCmd += " SINCE " + criteria.since.value();
1026
+ if (criteria.before.has_value()) searchCmd += " BEFORE " + criteria.before.value();
1027
+ if (criteria.on.has_value()) searchCmd += " ON " + criteria.on.value();
1028
+ if (criteria.sentSince.has_value()) searchCmd += " SENTSINCE " + criteria.sentSince.value();
1029
+ if (criteria.sentBefore.has_value()) searchCmd += " SENTBEFORE " + criteria.sentBefore.value();
1030
+ if (criteria.flagged) searchCmd += " FLAGGED";
1031
+ if (criteria.seen) searchCmd += " SEEN";
1032
+ if (criteria.answered) searchCmd += " ANSWERED";
1033
+ if (criteria.draft) searchCmd += " DRAFT";
1034
+ if (criteria.deleted) searchCmd += " DELETED";
1035
+ if (criteria.header.has_value()) searchCmd += " HEADER " + criteria.header.value().key + " \"" + criteria.header.value().value + "\"";
1036
+ if (criteria.larger.has_value()) searchCmd += " LARGER " + std::to_string(static_cast<long>(criteria.larger.value()));
1037
+ if (criteria.smaller.has_value()) searchCmd += " SMALLER " + std::to_string(static_cast<long>(criteria.smaller.value()));
1038
+ if (criteria.sortOrder.has_value() && toUpper(criteria.sortOrder.value()) == "ASC") {
1039
+ searchCmd += " SORT (DATE)";
1040
+ }
1041
+
1042
+ std::vector<std::string> responses;
1043
+ if (!sendCommandMulti(searchCmd, responses, error)) {
1044
+ return result;
1045
+ }
1046
+
1047
+ std::vector<long> uids;
1048
+ for (const auto& line : responses) {
1049
+ std::regex uidRegex(R"x(\*\s+SEARCH\s+(.+))x");
1050
+ std::smatch m;
1051
+ if (std::regex_match(line, m, uidRegex) && m.size() > 1) {
1052
+ std::istringstream uidStream(m[1].str());
1053
+ long uid;
1054
+ while (uidStream >> uid) {
1055
+ uids.push_back(uid);
1056
+ }
1057
+ }
1058
+ }
1059
+
1060
+ if (!criteria.sortOrder.has_value() || toUpper(criteria.sortOrder.value()) != "ASC") {
1061
+ std::sort(uids.begin(), uids.end(), std::greater<long>());
1062
+ }
1063
+
1064
+ if (criteria.limit.has_value() && criteria.limit.value() > 0) {
1065
+ size_t limit = static_cast<size_t>(criteria.limit.value());
1066
+ if (uids.size() > limit) uids.resize(limit);
1067
+ }
1068
+
1069
+ for (long uid : uids) {
1070
+ EmailMessage* msg = fetchEmailByUID(static_cast<double>(uid), error);
1071
+ if (msg) {
1072
+ if (criteria.hasAttachment) {
1073
+ std::string ct = toUpper(msg->mimeType);
1074
+ if (ct.find("MULTIPART/MIXED") == std::string::npos &&
1075
+ ct.find("MULTIPART/RELATED") == std::string::npos &&
1076
+ ct.find("MULTIPART/SIGNED") == std::string::npos) {
1077
+ delete msg;
1078
+ continue;
1079
+ }
1080
+ }
1081
+ result.push_back(*msg);
1082
+ delete msg;
1083
+ }
1084
+ }
1085
+
1086
+ return result;
1087
+ }
1088
+
1089
+ // ─── Flags ──────────────────────────────────────────────────
1090
+
1091
+ // 匹配 Android 行为:逐条处理,部分失败不阻断后续
1092
+ bool ImapClient::markAsRead(const std::vector<double>& uids, bool silent, std::string& error) {
1093
+ if (!m_state.selected) { error = "No mailbox selected"; return false; }
1094
+
1095
+ for (double uid : uids) {
1096
+ std::string response, cmdError;
1097
+ sendCommand("UID STORE " + std::to_string(static_cast<long>(uid)) + " +FLAGS.SILENT (\\Seen)", response, cmdError);
1098
+ }
1099
+ return true;
1100
+ }
1101
+
1102
+ bool ImapClient::markAsFlagged(const std::vector<double>& uids, bool flagged, std::string& error) {
1103
+ if (!m_state.selected) { error = "No mailbox selected"; return false; }
1104
+
1105
+ std::string cmd = flagged ? "+FLAGS.SILENT (\\Flagged)" : "-FLAGS.SILENT (\\Flagged)";
1106
+ for (double uid : uids) {
1107
+ std::string response, cmdError;
1108
+ sendCommand("UID STORE " + std::to_string(static_cast<long>(uid)) + " " + cmd, response, cmdError);
1109
+ }
1110
+ return true;
1111
+ }
1112
+
1113
+ // ─── Move / Copy / Delete ───────────────────────────────────
1114
+
1115
+ bool ImapClient::moveEmails(const std::vector<double>& uids, const std::string& destinationMailbox, std::string& error) {
1116
+ if (!m_state.selected) { error = "No mailbox selected"; return false; }
1117
+
1118
+ std::string uidList;
1119
+ for (double uid : uids) {
1120
+ if (!uidList.empty()) uidList += ",";
1121
+ uidList += std::to_string(static_cast<long>(uid));
1122
+ }
1123
+
1124
+ std::string response;
1125
+ if (sendCommand("UID MOVE " + uidList + " \"" + destinationMailbox + "\"", response, error)) {
1126
+ return true;
1127
+ }
1128
+
1129
+ error = "";
1130
+ if (!sendCommand("UID COPY " + uidList + " \"" + destinationMailbox + "\"", response, error)) {
1131
+ return false;
1132
+ }
1133
+ error = "";
1134
+ if (!sendCommand("UID STORE " + uidList + " +FLAGS.SILENT (\\Deleted)", response, error)) {
1135
+ return false;
1136
+ }
1137
+ return expunge(error);
1138
+ }
1139
+
1140
+ bool ImapClient::copyEmails(const std::vector<double>& uids, const std::string& destinationMailbox, std::string& error) {
1141
+ if (!m_state.selected) { error = "No mailbox selected"; return false; }
1142
+
1143
+ std::string uidList;
1144
+ for (double uid : uids) {
1145
+ if (!uidList.empty()) uidList += ",";
1146
+ uidList += std::to_string(static_cast<long>(uid));
1147
+ }
1148
+ std::string response;
1149
+ return sendCommand("UID COPY " + uidList + " \"" + destinationMailbox + "\"", response, error);
1150
+ }
1151
+
1152
+ DeleteResult ImapClient::deleteEmails(const std::vector<double>& uids, std::string& error) {
1153
+ DeleteResult result;
1154
+
1155
+ if (!m_state.selected) { error = "No mailbox selected"; result.error = error; return result; }
1156
+
1157
+ // 逐条 UID 处理,匹配 Android 行为
1158
+ for (double uid : uids) {
1159
+ bool retried = false;
1160
+ while (true) {
1161
+ std::string cmd = "UID STORE " + std::to_string(static_cast<long>(uid)) + " +FLAGS.SILENT (\\Deleted)";
1162
+ std::string response;
1163
+ if (sendCommand(cmd, response, error)) {
1164
+ result.success.push_back(uid);
1165
+ break;
1166
+ }
1167
+ // 若文件夹被服务器关闭(类似 Android 的 FolderClosedException),重选后重试一次
1168
+ if (!retried && !m_state.selected) {
1169
+ retried = true;
1170
+ // 重新选择邮箱(匹配 Android 的 ensureFolderOpen)
1171
+ if (!m_state.selectedMailbox.empty()) {
1172
+ selectMailbox(m_state.selectedMailbox, false, error);
1173
+ }
1174
+ continue;
1175
+ }
1176
+ result.failed.push_back(uid);
1177
+ break;
1178
+ }
1179
+ }
1180
+
1181
+ // 有成功删除的邮件才执行 EXPUNGE
1182
+ if (!result.success.empty()) {
1183
+ std::string expungeError;
1184
+ expunge(expungeError);
1185
+ }
1186
+
1187
+ // 有失败时设置错误信息,匹配 Android 行为
1188
+ if (!result.failed.empty()) {
1189
+ result.error = std::to_string(result.failed.size()) + " messages failed";
1190
+ if (!error.empty()) result.error = result.error.value() + ": " + error;
1191
+ }
1192
+
1193
+ return result;
1194
+ }
1195
+
1196
+ bool ImapClient::expunge(std::string& error) {
1197
+ if (!m_state.selected) return true;
1198
+ std::string response;
1199
+ return sendCommand("EXPUNGE", response, error);
1200
+ }
1201
+
1202
+ // ─── Attachments ────────────────────────────────────────────
1203
+
1204
+ AttachmentInfo* ImapClient::fetchAttachment(double uid, const std::string& partId, std::string& error) {
1205
+ std::vector<std::string> responses;
1206
+ std::string cmd = "UID FETCH " + std::to_string(static_cast<long>(uid)) +
1207
+ " (BODY.PEEK[" + partId + "])";
1208
+
1209
+ if (!sendCommandMulti(cmd, responses, error)) return nullptr;
1210
+
1211
+ std::string bodyContent;
1212
+ bool inPart = false;
1213
+ for (const auto& line : responses) {
1214
+ if (line.find("BODY[" + partId + "]") != std::string::npos) { inPart = true; continue; }
1215
+ if (line == ")") { inPart = false; continue; }
1216
+ if (inPart) bodyContent += line + "\r\n";
1217
+ }
1218
+
1219
+ if (bodyContent.empty()) return nullptr;
1220
+
1221
+ AttachmentInfo* info = new AttachmentInfo();
1222
+ info->filename = "attachment_" + partId;
1223
+ info->mimeType = "application/octet-stream";
1224
+ info->partId = partId;
1225
+ info->size = static_cast<double>(bodyContent.size());
1226
+ info->data.assign(bodyContent.begin(), bodyContent.end()); // ArrayBuffer::copy → vector<uint8_t>
1227
+ info->contentId = std::nullopt;
1228
+ info->isInline = false;
1229
+ info->charset = std::nullopt;
1230
+ info->contentDisposition = "attachment";
1231
+ info->localPath = std::nullopt;
1232
+
1233
+ return info;
1234
+ }
1235
+
1236
+ std::vector<AttachmentInfo> ImapClient::fetchAllAttachments(double uid, std::string& error) {
1237
+ std::vector<AttachmentInfo> result;
1238
+
1239
+ std::vector<std::string> responses;
1240
+ std::string cmd = "UID FETCH " + std::to_string(static_cast<long>(uid)) + " (BODYSTRUCTURE)";
1241
+ if (!sendCommandMulti(cmd, responses, error)) return result;
1242
+
1243
+ cmd = "UID FETCH " + std::to_string(static_cast<long>(uid)) + " (BODY.PEEK[]<0.10485760>)";
1244
+ responses.clear();
1245
+ if (!sendCommandMulti(cmd, responses, error)) return result;
1246
+
1247
+ std::string fullContent;
1248
+ bool inBody = false;
1249
+ for (const auto& line : responses) {
1250
+ if (line.find("BODY[]") != std::string::npos) { inBody = true; continue; }
1251
+ if (line == ")") { inBody = false; continue; }
1252
+ if (inBody) fullContent += line + "\r\n";
1253
+ }
1254
+
1255
+ if (fullContent.empty()) return result;
1256
+
1257
+ std::regex attachmentRegex(R"re(Content-Disposition:\s*attachment[^;]*;\s*filename\s*=\s*"?([^";\r\n]+)"?")re",
1258
+ std::regex::icase);
1259
+ auto begin = std::sregex_iterator(fullContent.begin(), fullContent.end(), attachmentRegex);
1260
+ auto end = std::sregex_iterator();
1261
+
1262
+ for (auto it = begin; it != end; ++it) {
1263
+ AttachmentInfo att;
1264
+ att.filename = (*it)[1];
1265
+ att.mimeType = "application/octet-stream";
1266
+ att.partId = "attachment_" + std::to_string(result.size());
1267
+ att.size = 0;
1268
+ att.data = std::vector<uint8_t>(); // ArrayBuffer::copy(nullptr, 0) → empty vector
1269
+ att.contentId = std::nullopt;
1270
+ att.isInline = false;
1271
+ att.charset = std::nullopt;
1272
+ att.contentDisposition = "attachment";
1273
+ att.localPath = std::nullopt;
1274
+ result.push_back(att);
1275
+ }
1276
+
1277
+ return result;
1278
+ }
1279
+
1280
+ // ─── IDLE ───────────────────────────────────────────────────
1281
+
1282
+ bool ImapClient::idle(const std::function<void(const IdleNotification&)>& callback, std::string& error) {
1283
+ if (!m_state.selected) { error = "No mailbox selected"; return false; }
1284
+
1285
+ std::string tag = nextTag();
1286
+ std::string cmd = tag + " IDLE\r\n";
1287
+ if (!m_socket->send(cmd)) { error = "Failed to send IDLE command"; return false; }
1288
+
1289
+ std::string response = receiveLine();
1290
+ if (response.empty() || response[0] != '+') { error = "IDLE not accepted by server"; return false; }
1291
+
1292
+ return true;
1293
+ }
1294
+
1295
+ bool ImapClient::stopIdle(std::string& error) {
1296
+ if (!m_socket->send("DONE\r\n")) { error = "Failed to send DONE"; return false; }
1297
+
1298
+ std::string response;
1299
+ return waitForTaggedResponse(m_state.tagPrefix + std::to_string(m_state.tagCounter), response, error);
1300
+ }
1301
+
1302
+ // ─── Quota ──────────────────────────────────────────────────
1303
+
1304
+ QuotaInfo ImapClient::getQuota(const std::optional<std::string>& root, std::string& error) {
1305
+ QuotaInfo info;
1306
+ info.storageUsed = 0;
1307
+ info.storageLimit = -1;
1308
+ info.storageUnit = "KB";
1309
+ info.root = root.value_or("");
1310
+
1311
+ std::string quotaRoot = root.value_or("");
1312
+ if (quotaRoot.empty()) quotaRoot = "\"\"";
1313
+ else quotaRoot = "\"" + quotaRoot + "\"";
1314
+
1315
+ std::vector<std::string> responses;
1316
+ sendCommandMulti("GETQUOTA " + quotaRoot, responses, error);
1317
+
1318
+ for (const auto& line : responses) {
1319
+ std::regex quotaRegex(R"x(STORAGE\s+(\d+)\s+(\d+))x");
1320
+ std::smatch m;
1321
+ if (std::regex_search(line, m, quotaRegex) && m.size() > 2) {
1322
+ info.storageUsed = std::stod(m[1]);
1323
+ info.storageLimit = std::stod(m[2]);
1324
+ }
1325
+ }
1326
+
1327
+ return info;
1328
+ }
1329
+
1330
+ // ─── Append ─────────────────────────────────────────────────
1331
+
1332
+ AppendResult ImapClient::appendMessage(const std::string& mailbox,
1333
+ const std::vector<uint8_t>& rawMimeData,
1334
+ const std::optional<std::vector<std::string>>& flags,
1335
+ std::string& error) {
1336
+ AppendResult result;
1337
+ result.uid = -1;
1338
+ result.uidValidity = 0;
1339
+
1340
+ std::string mimeData(rawMimeData.begin(), rawMimeData.end());
1341
+
1342
+ std::string flagStr;
1343
+ if (flags.has_value() && !flags.value().empty()) {
1344
+ flagStr = " (";
1345
+ for (const auto& flag : flags.value()) {
1346
+ std::string upper = toUpper(flag);
1347
+ if (upper == "SEEN") flagStr += "\\Seen ";
1348
+ else if (upper == "FLAGGED") flagStr += "\\Flagged ";
1349
+ else if (upper == "DELETED") flagStr += "\\Deleted ";
1350
+ else if (upper == "DRAFT") flagStr += "\\Draft ";
1351
+ else if (upper == "ANSWERED") flagStr += "\\Answered ";
1352
+ else flagStr += flag + " ";
1353
+ }
1354
+ flagStr.pop_back();
1355
+ flagStr += ")";
1356
+ }
1357
+
1358
+ std::string tag = nextTag();
1359
+ std::string cmd = tag + " APPEND \"" + mailbox + "\"" + flagStr + " {" +
1360
+ std::to_string(mimeData.size()) + "}\r\n";
1361
+
1362
+ if (!m_socket->send(cmd)) { error = "Failed to send APPEND command"; return result; }
1363
+
1364
+ std::string response = receiveLine();
1365
+ if (response.empty() || response[0] != '+') { error = "APPEND not accepted"; return result; }
1366
+
1367
+ if (!m_socket->send(mimeData + "\r\n")) { error = "Failed to send MIME data"; return result; }
1368
+
1369
+ if (!waitForTaggedResponse(tag, response, error)) return result;
1370
+
1371
+ std::regex uidRegex(R"x(\[APPENDUID\s+(\d+)\s+(\d+)\])x");
1372
+ std::smatch m;
1373
+ if (std::regex_search(response, m, uidRegex) && m.size() > 2) {
1374
+ result.uidValidity = std::stod(m[1]);
1375
+ result.uid = std::stod(m[2]);
1376
+ }
1377
+
1378
+ result.date = currentTimestamp();
1379
+ return result;
1380
+ }
1381
+
1382
+ } // namespace emailimap