@bobfrankston/rmfmail 1.2.272 → 1.2.276

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 (63) hide show
  1. package/.commitmsg +26 -0
  2. package/client/android-bootstrap.bundle.js +184 -0
  3. package/client/android-bootstrap.bundle.js.map +2 -2
  4. package/client/app.bundle.js +235 -31
  5. package/client/app.bundle.js.map +4 -4
  6. package/client/app.js +35 -1
  7. package/client/app.js.map +1 -1
  8. package/client/app.ts +36 -1
  9. package/client/components/invite-card.js +107 -0
  10. package/client/components/invite-card.js.map +1 -0
  11. package/client/components/invite-card.ts +119 -0
  12. package/client/components/message-list.js +9 -1
  13. package/client/components/message-list.js.map +1 -1
  14. package/client/components/message-list.ts +9 -1
  15. package/client/components/message-viewer.js +82 -19
  16. package/client/components/message-viewer.js.map +1 -1
  17. package/client/components/message-viewer.ts +79 -18
  18. package/client/compose/compose.bundle.js +4 -0
  19. package/client/compose/compose.bundle.js.map +2 -2
  20. package/client/lib/api-client.js +6 -0
  21. package/client/lib/api-client.js.map +1 -1
  22. package/client/lib/api-client.ts +6 -0
  23. package/client/lib/approved-senders.js +66 -0
  24. package/client/lib/approved-senders.js.map +1 -0
  25. package/client/lib/approved-senders.ts +63 -0
  26. package/client/lib/mailxapi.js +3 -0
  27. package/client/package.json +1 -1
  28. package/client/styles/components.css +48 -0
  29. package/package.json +10 -10
  30. package/packages/mailx-imap/package-lock.json +2 -2
  31. package/packages/mailx-imap/package.json +2 -2
  32. package/packages/mailx-service/index.d.ts +15 -0
  33. package/packages/mailx-service/index.d.ts.map +1 -1
  34. package/packages/mailx-service/index.js +18 -0
  35. package/packages/mailx-service/index.js.map +1 -1
  36. package/packages/mailx-service/index.ts +19 -0
  37. package/packages/mailx-service/jsonrpc.js +2 -0
  38. package/packages/mailx-service/jsonrpc.js.map +1 -1
  39. package/packages/mailx-service/jsonrpc.ts +2 -0
  40. package/packages/mailx-service/package.json +1 -1
  41. package/packages/mailx-settings/package.json +1 -1
  42. package/packages/mailx-store/package.json +1 -1
  43. package/packages/mailx-store/store.d.ts +5 -0
  44. package/packages/mailx-store/store.d.ts.map +1 -1
  45. package/packages/mailx-store/store.js +21 -0
  46. package/packages/mailx-store/store.js.map +1 -1
  47. package/packages/mailx-store/store.ts +27 -0
  48. package/packages/mailx-store-web/android-bootstrap.js +1 -0
  49. package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
  50. package/packages/mailx-store-web/android-bootstrap.ts +1 -0
  51. package/packages/mailx-store-web/package.json +1 -1
  52. package/packages/mailx-store-web/web-service.d.ts +11 -0
  53. package/packages/mailx-store-web/web-service.d.ts.map +1 -1
  54. package/packages/mailx-store-web/web-service.js +14 -0
  55. package/packages/mailx-store-web/web-service.js.map +1 -1
  56. package/packages/mailx-store-web/web-service.ts +15 -0
  57. package/packages/mailx-types/index.d.ts +56 -0
  58. package/packages/mailx-types/index.d.ts.map +1 -1
  59. package/packages/mailx-types/index.js +187 -0
  60. package/packages/mailx-types/index.js.map +1 -1
  61. package/packages/mailx-types/index.ts +192 -0
  62. package/packages/mailx-types/package.json +1 -1
  63. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-72180 → node_modules.npmglobalize-stash-59216}/.package-lock.json +0 -0
@@ -848,3 +848,195 @@ export function formatSender(addr: { name?: string; address: string } | null | u
848
848
  if (name.toLowerCase() === address.toLowerCase()) return { text: address, claimed: null };
849
849
  return { text: `${name} <${address}>`, claimed: displayNameClaimsOtherAddress(name, address) };
850
850
  }
851
+
852
+
853
+ // ── Calendar invites (text/calendar) ──────────────────────────────────────
854
+
855
+ export interface InviteAttendee {
856
+ name: string;
857
+ email: string;
858
+ /** NEEDS-ACTION | ACCEPTED | DECLINED | TENTATIVE | DELEGATED */
859
+ status: string;
860
+ rsvp: boolean;
861
+ }
862
+
863
+ export interface ParsedInvite {
864
+ /** REQUEST (an invitation), REPLY (someone's RSVP), CANCEL, PUBLISH… */
865
+ method: string;
866
+ uid: string;
867
+ sequence: number;
868
+ summary: string;
869
+ description: string;
870
+ location: string;
871
+ /** ISO 8601. All-day events carry a bare date (no time part). */
872
+ start: string;
873
+ end: string;
874
+ /** IANA zone from TZID, when the invite specified one. */
875
+ tzid: string;
876
+ allDay: boolean;
877
+ organizer: { name: string; email: string };
878
+ attendees: InviteAttendee[];
879
+ /** Raw RRULE, if the event repeats. Rendered verbatim — turning it into
880
+ * prose is a separate job and getting it subtly wrong is worse than
881
+ * showing the rule. */
882
+ rrule: string;
883
+ status: string;
884
+ /** True when the invite was produced by Google Calendar. Google's own
885
+ * HTML body carries working Yes/No/Maybe links, so the UI must NOT add a
886
+ * second set of RSVP controls for these — see inviteHasGoogleRsvp. */
887
+ fromGoogle: boolean;
888
+ }
889
+
890
+ /** Unfold RFC 5545 continuation lines: a line beginning with a space or tab
891
+ * continues the previous one. Google folds ATTENDEE lines mid-parameter, so
892
+ * skipping this loses attendees entirely. */
893
+ function unfoldIcs(text: string): string[] {
894
+ const raw = (text || "").replace(/\r\n/g, "\n").split("\n");
895
+ const out: string[] = [];
896
+ for (const line of raw) {
897
+ if (/^[ \t]/.test(line) && out.length) out[out.length - 1] += line.slice(1);
898
+ else out.push(line);
899
+ }
900
+ return out;
901
+ }
902
+
903
+ /** RFC 5545 TEXT unescaping: \\n is a newline, and \\, \\; \\, are literals. */
904
+ function unescapeIcs(v: string): string {
905
+ return (v || "")
906
+ .replace(/\\n/gi, "\n")
907
+ .replace(/\\([;,\\])/g, "$1");
908
+ }
909
+
910
+ /** Split "KEY;PARAM=x;PARAM2=y:value" into its three pieces. */
911
+ function splitIcsLine(line: string): { key: string; params: Record<string, string>; value: string } | null {
912
+ // The first colon that is not inside a quoted parameter ends the name part.
913
+ let inQuote = false, colon = -1;
914
+ for (let i = 0; i < line.length; i++) {
915
+ const c = line[i];
916
+ if (c === '"') inQuote = !inQuote;
917
+ else if (c === ":" && !inQuote) { colon = i; break; }
918
+ }
919
+ if (colon < 0) return null;
920
+ const namePart = line.slice(0, colon);
921
+ const value = line.slice(colon + 1);
922
+ const bits = namePart.split(";");
923
+ const key = (bits.shift() || "").toUpperCase();
924
+ const params: Record<string, string> = {};
925
+ for (const b of bits) {
926
+ const eq = b.indexOf("=");
927
+ if (eq < 0) continue;
928
+ params[b.slice(0, eq).toUpperCase()] = b.slice(eq + 1).replace(/^"|"$/g, "");
929
+ }
930
+ return { key, params, value };
931
+ }
932
+
933
+ /** ICS timestamp → ISO 8601. Handles 20260823T080000 (local/zoned),
934
+ * 20260823T133939Z (UTC) and 20260823 (all-day). Zoned values keep their
935
+ * wall-clock reading; the caller pairs them with `tzid`. */
936
+ function icsDateToIso(v: string, isUtc: boolean): string {
937
+ const m = (v || "").match(/^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})(Z)?)?$/);
938
+ if (!m) return "";
939
+ const [, y, mo, d, hh, mm, ss, z] = m;
940
+ if (!hh) return `${y}-${mo}-${d}`;
941
+ const base = `${y}-${mo}-${d}T${hh}:${mm}:${ss}`;
942
+ return (z || isUtc) ? base + "Z" : base;
943
+ }
944
+
945
+ function parseCalAddress(value: string, params: Record<string, string>): { name: string; email: string } {
946
+ const email = (value || "").replace(/^mailto:/i, "").trim();
947
+ return { name: (params.CN || "").trim() || email, email };
948
+ }
949
+
950
+ /**
951
+ * Parse the VEVENT out of a text/calendar part.
952
+ *
953
+ * Deliberately hand-rolled rather than pulling in an ICS library: mailx-types
954
+ * is the zero-dependency package shared with Android, and an invite card needs
955
+ * a dozen properties, not full RFC 5545 coverage. Returns null when there is
956
+ * no VEVENT to show.
957
+ */
958
+ export function parseInvite(ics: string): ParsedInvite | null {
959
+ if (!ics || !/BEGIN:VEVENT/i.test(ics)) return null;
960
+ const lines = unfoldIcs(ics);
961
+
962
+ const out: ParsedInvite = {
963
+ method: "", uid: "", sequence: 0, summary: "", description: "", location: "",
964
+ start: "", end: "", tzid: "", allDay: false,
965
+ organizer: { name: "", email: "" }, attendees: [], rrule: "", status: "",
966
+ fromGoogle: false,
967
+ };
968
+
969
+ let inEvent = false;
970
+ // VTIMEZONE blocks contain their own DTSTART/RRULE — those describe the
971
+ // zone's DST rules, not the meeting. Ignore everything inside them.
972
+ let depthNonEvent = 0;
973
+
974
+ for (const line of lines) {
975
+ const up = line.toUpperCase();
976
+ if (up.startsWith("BEGIN:VEVENT")) { inEvent = true; continue; }
977
+ if (up.startsWith("END:VEVENT")) { inEvent = false; continue; }
978
+ if (!inEvent && (up.startsWith("BEGIN:VTIMEZONE") || up.startsWith("BEGIN:VALARM"))) { depthNonEvent++; continue; }
979
+ if (up.startsWith("END:VTIMEZONE") || up.startsWith("END:VALARM")) { if (depthNonEvent) depthNonEvent--; continue; }
980
+ if (inEvent && up.startsWith("BEGIN:VALARM")) { depthNonEvent++; continue; }
981
+
982
+ const p = splitIcsLine(line);
983
+ if (!p) continue;
984
+
985
+ if (!inEvent) {
986
+ if (p.key === "METHOD") out.method = p.value.trim().toUpperCase();
987
+ else if (p.key === "PRODID" && /google/i.test(p.value)) out.fromGoogle = true;
988
+ continue;
989
+ }
990
+ if (depthNonEvent) continue; // inside VALARM
991
+
992
+ switch (p.key) {
993
+ case "SUMMARY": out.summary = unescapeIcs(p.value); break;
994
+ case "DESCRIPTION": out.description = unescapeIcs(p.value); break;
995
+ case "LOCATION": out.location = unescapeIcs(p.value); break;
996
+ case "UID": out.uid = p.value.trim(); break;
997
+ case "STATUS": out.status = p.value.trim().toUpperCase(); break;
998
+ case "RRULE": out.rrule = p.value.trim(); break;
999
+ case "SEQUENCE": out.sequence = parseInt(p.value, 10) || 0; break;
1000
+ case "DTSTART":
1001
+ out.start = icsDateToIso(p.value.trim(), false);
1002
+ out.tzid = p.params.TZID || out.tzid;
1003
+ out.allDay = (p.params.VALUE || "").toUpperCase() === "DATE" || !/T/.test(p.value);
1004
+ break;
1005
+ case "DTEND":
1006
+ out.end = icsDateToIso(p.value.trim(), false);
1007
+ out.tzid = out.tzid || p.params.TZID || "";
1008
+ break;
1009
+ case "ORGANIZER":
1010
+ out.organizer = parseCalAddress(p.value, p.params);
1011
+ break;
1012
+ case "ATTENDEE": {
1013
+ const a = parseCalAddress(p.value, p.params);
1014
+ out.attendees.push({
1015
+ name: a.name,
1016
+ email: a.email,
1017
+ status: (p.params.PARTSTAT || "NEEDS-ACTION").toUpperCase(),
1018
+ rsvp: (p.params.RSVP || "").toUpperCase() === "TRUE",
1019
+ });
1020
+ break;
1021
+ }
1022
+ }
1023
+ }
1024
+
1025
+ if (!out.summary && !out.start) return null;
1026
+ return out;
1027
+ }
1028
+
1029
+ /**
1030
+ * Does the message body already carry Google's own Yes/No/Maybe controls?
1031
+ *
1032
+ * Google Calendar invitations embed working RSVP links
1033
+ * (`calendar.google.com/calendar/event?action=RESPOND&rst=1|2|3`). When they
1034
+ * are present the UI must show the invite summary but NOT a second set of RSVP
1035
+ * buttons — two responders racing to set PARTSTAT is worse than one
1036
+ * (Bob 2026-08-22: "Google's own responder handles it so you might detect that
1037
+ * case specially").
1038
+ */
1039
+ export function inviteHasGoogleRsvp(bodyHtml: string): boolean {
1040
+ if (!bodyHtml) return false;
1041
+ return /calendar\.google\.com\/calendar\/event\?action=RESPOND/i.test(bodyHtml);
1042
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-types",
3
- "version": "0.1.57",
3
+ "version": "0.1.59",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",