diff --git a/include/lgi/common/Store3Defs.h b/include/lgi/common/Store3Defs.h --- a/include/lgi/common/Store3Defs.h +++ b/include/lgi/common/Store3Defs.h @@ -1,455 +1,456 @@ #ifndef _STORE3_DEFS_H_ #define _STORE3_DEFS_H_ ////////////////////////////////////////////////////////////////////////////////////////////////////// // CORE DEFINITIONS (relevant to any implementation) ////////////////////////////////////////////////////////////////////////////////////////////////////// /// Load state /// /// This is used by both objects AND iterators enum Store3State { /// The object is not loaded at all.. Store3Unloaded, /// The object is currently loading (in some worker thread or something) /// Best to not try and access anything like data fields. Store3Loading, /// The mail object has only it's headers loaded, and not the body. Store3Headers, /// The object is fully loaded and can be accessed normally. Store3Loaded, }; inline const char *toString(Store3State s) { #define _(s) case s: return #s; switch (s) { _(Store3Unloaded) _(Store3Loading) _(Store3Headers) _(Store3Loaded) } #undef _ LAssert(0); return "#invalidStore3State"; } /// Folder system type enum Store3SystemFolder { Store3SystemNone, Store3SystemInbox, Store3SystemTrash, Store3SystemOutbox, Store3SystemSent, Store3SystemCalendar, Store3SystemContacts, Store3SystemSpam, }; inline const char *toString(Store3SystemFolder s) { #define _(s) case s: return #s; switch (s) { _(Store3SystemNone) _(Store3SystemInbox) _(Store3SystemTrash) _(Store3SystemOutbox) _(Store3SystemSent) _(Store3SystemCalendar) _(Store3SystemContacts) _(Store3SystemSpam) } #undef _ LAssert(0); return "#invalidStore3SystemFolder"; } /// This defines the possible outcomes of calling a function. enum Store3Status { // Open mail store specific: //-------------------------------------------------------------------------------- /// The store file is missing Store3Missing = -1, /// A format upgrade is required to open the store. Store3UpgradeRequired = -2, /// Function not implemented. Store3NotImpl = -3, /// Missing permissions for operation. Store3NoPermissions = -4, // General: //-------------------------------------------------------------------------------- /// The method failed and no action was taken. Store3Error = 0, /// The method succeeded but the action was not completed immediately, notification /// of the actions completion will come later via the callback interface. Store3Delayed = 1, /// The method succeeded and the action has been already completed. Store3Success = 2, }; /// Possible parts of UI enum Store3UiFields { Store3UiCurrentPos, // [Set] sets the current progress value Store3UiMaxPos, // [Set] sets the maximum progress value Store3UiStatus, // [Set] set a status/progress string Store3UiError, // [Set] set an error string Store3UiInteractive, // [Get] returns a bool if the user is expecting interaction Store3UiCancel, // [Get] returns a bool indicating if the user has cancelled the operation Store3UiNewFormat, // [Get] returns a integer/enum describing the new format to use }; enum Store3Backend { Store3Sqlite, Store3Imap, Store3Mapi, Store3Webdav, }; ////////////////////////////////////////////////////////////////////////////////////////////////////// // MAIL/CALENDAR CLIENT RELATED DEFINITIONS ////////////////////////////////////////////////////////////////////////////////////////////////////// enum EmailFlags { // Mail flags MAIL_SENT = (1 << 0), MAIL_RECEIVED = (1 << 1), MAIL_CREATED = (1 << 2), MAIL_FORWARDED = (1 << 3), MAIL_REPLIED = (1 << 4), MAIL_ATTACHMENTS = (1 << 5), MAIL_READ = (1 << 6), // #define MAIL_MARK (1 << 7), // Deprecated MAIL_READY_TO_SEND = (1 << 8), // If this flag is set then the user // wants to send the mail on the next // run. When the user just saves a new // message in the outbox this isn't set // and isn't sent until they go in and // say "send". At which point this flag // is set and the message sent MAIL_READ_RECEIPT = (1 << 9), MAIL_IGNORE = (1 << 10), MAIL_FIXED_WIDTH_FONT = (1 << 11), MAIL_BOUNCED = (1 << 12), // The bounce source mail MAIL_BOUNCE = (1 << 13), // The outgoing copy of a bounced mail MAIL_SHOW_IMAGES = (1 << 14), // User selected to show images in HTML MAIL_NEW = (1 << 18), // Mail is new, cleared after the OnNew event happens MAIL_STORED_FLAT = (1 << 19), // Message is signed and/or encrypted and needs // to be stored in such a way as the RFC822 image // is not damaged. // Bayesian filter flags: MAIL_BAYES_HAM = (1 << 16), // Bayesian classified originally as ham MAIL_BAYES_SPAM = (1 << 17), // Bayesian classified originally as spam MAIL_HAM_DB = (1 << 20), // In Bayesian ham word database MAIL_SPAM_DB = (1 << 21), // In Bayesian spam word database }; extern LString EmailFlagsToStr(int flags); enum EmailAddressType { MAIL_ADDR_TO = 0, MAIL_ADDR_CC = 1, MAIL_ADDR_BCC = 2, MAIL_ADDR_FROM = 3, }; enum EmailPriority { // FIELD_PRIORITY is equivilent to the header field: X-Priority // 1 - High // ... // 5 - Low MAIL_PRIORITY_HIGH = 1, MAIL_PRIORITY_NORMAL = 3, MAIL_PRIORITY_LOW = 5, }; enum CalendarReminderType { CalEmail, CalPopup, CalScriptCallback, CalMaxType, }; enum CalendarReminderUnits { CalMinutes, CalHours, CalDays, CalWeeks, CalMaxUnit, }; enum CalendarShowTimeAs { CalFree = 0, CalTentative = 1, CalBusy = 2, CalOut = 3 }; enum CalendarType { CalEvent, CalTodo, CalJournal, CalRequest, CalReply, }; enum CalendarPrivacyType { CalDefaultPriv = 0, CalPublic, CalPrivate }; // To convert to string use 'Store3ItemTypeName' enum Store3ItemTypes { MAGIC_NONE = 0x00000000, MAGIC_BASE = 0xAAFF0000, MAGIC_MAIL = (MAGIC_BASE+1), // Mail item MAGIC_CONTACT = (MAGIC_BASE+2), // Contact item MAGIC_FOLDER_OLD = (MAGIC_BASE+3), // Old Store1 folder of items MAGIC_MAILBOX = (MAGIC_BASE+4), // Root of mail tree (nothing-abstract) MAGIC_ATTACHMENT = (MAGIC_BASE+5), // Mail attachment MAGIC_ANY = (MAGIC_BASE+6), // Trash folder type (accepts any object) MAGIC_FILTER = (MAGIC_BASE+7), // Used to match messages against MAGIC_FOLDER = (MAGIC_BASE+8), // Folder v2 MAGIC_CONDITION = (MAGIC_BASE+9), // Filter condition MAGIC_ACTION = (MAGIC_BASE+10), // Filter action MAGIC_CALENDAR = (MAGIC_BASE+11), // Calendar event MAGIC_ATTENDEE = (MAGIC_BASE+12), // Event attendee MAGIC_GROUP = (MAGIC_BASE+13), // Group of contacts MAGIC_CALENDAR_FILE = (MAGIC_BASE+14), // Calendar attachment MAGIC_MAX = (MAGIC_BASE+15) // One past the end }; extern const char *Store3ItemTypeName(Store3ItemTypes t); // When setting this via LDataI::SetInt the return value is: // - true if you need to mark the object dirty so that it gets saved // - false if the flags is stored elsewhere and you don't have to save. enum Store3Fields { FIELD_NULL = 0, FIELD_FLAGS = 1, // (EmailFlags) The message flags FIELD_TO = 2, // (DIterator) List of recipients FIELD_CC = 3, // (EmailAddressType) FIELD_FROM = 4, // (Store3Addr*) The From address FIELD_REPLY = 5, // (Store3Addr*) The Reply-To address FIELD_SUBJECT = 6, // (char*) Subject of the message FIELD_TEXT = 7, // (char*) Textual body FIELD_MESSAGE_ID = 8, // (char*) The message ID FIELD_DATE_RECEIVED = 9, // (LDateTime*) Received date FIELD_INTERNET_HEADER = 10, // (char*) The internet headers // Contact fields FIELD_FIRST_NAME = 11, // (char*) First name FIELD_LAST_NAME = 12, // (char*) Last/surname FIELD_EMAIL = 13, // (char*) The default email address // \sa 'FIELD_ALT_EMAIL' FIELD_HOME_STREET = 14, // (char*) FIELD_HOME_SUBURB = 15, // (char*) FIELD_HOME_POSTCODE = 16, // (char*) FIELD_HOME_STATE = 17, // (char*) FIELD_HOME_COUNTRY = 18, // (char*) FIELD_WORK_PHONE = 19, // (char*) This was already defined FIELD_HOME_PHONE = 20, // (char*) FIELD_HOME_MOBILE = 21, // (char*) FIELD_HOME_IM = 22, // (char*) FIELD_HOME_FAX = 23, // (char*) FIELD_HOME_WEBPAGE = 24, // (char*) FIELD_NICK = 25, // (char*) FIELD_SPOUSE = 26, // (char*) FIELD_NOTE = 27, // (char*) FIELD_PLUGIN_ASSOC = 28, // (char*) // More fields FIELD_SIZE = 29, // (uint64) FIELD_DATE_SENT = 30, // (LDateTime*) FIELD_COLUMN = 31, // (uint64) // FIELD_BCC = 32, // Deprecated FIELD_MIME_TYPE = 33, // (char*) The MIME type FIELD_PRIORITY = 34, // (EmailPriority) FIELD_FOLDER_OPEN = 35, // (bool) True if the folder is expanded to show child folders // FIELD_CODE_PAGE = 36, // Deprecated FIELD_COLOUR = 37, // (uint32) Rgba32 FIELD_ALTERNATE_HTML = 38, // (char*) The HTML part of the message FIELD_CONTENT_ID = 39, // (char*) An attachment content ID // Filter fields FIELD_FILTER_NAME = 40, // (char*) // FIELD_ACT_TYPE = 46, // Deprecated // FIELD_ACT_ARG = 47, // Deprecated // FIELD_DIGEST_INDEX = 48, // Deprecated FIELD_COMBINE_OP = 49, FIELD_FILTER_INDEX = 50, FIELD_FILTER_SCRIPT = 52, FIELD_STOP_FILTERING = 54, FIELD_FILTER_INCOMING = 55, FIELD_FILTER_OUTGOING = 56, FIELD_FILTER_CONDITIONS_XML = 57, FIELD_FILTER_ACTIONS_XML = 58, FIELD_FILTER_INTERNAL = 59, // Calendar fields // FIELD_CAL_START_LOCAL = 60, // **deprecated** // FIELD_CAL_END_LOCAL = 61, // **deprecated** FIELD_CAL_SUBJECT = 62, // (char*) event title FIELD_CAL_LOCATION = 63, // (char*) location of event FIELD_CAL_ALL_DAY = 64, // (bool) All Day setting // FIELD_CAL_REMINDER_ACTION = 65, // **deprecated** (CalendarReminderType) The reminder type // FIELD_CAL_REMINDER_UNIT = 66, // **deprecated** (CalendarReminderUnits) The unit of time FIELD_CAL_SHOW_TIME_AS = 67, // (CalendarShowTimeAs) Busy/Free etc FIELD_CAL_RECUR_FREQ = 68, // (CalRecurFreq) Base time unit of recurring: e.g. Day/Week/Month/Year. FIELD_CAL_RECUR_INTERVAL = 69, // (int) Number of FIELD_CAL_RECUR_FREQ units of time between recurring events. FIELD_CAL_RECUR_FILTER_DAYS = 70 , // (int) Bitmask of days: Bit 0 = Sunday, Bit 1 = Monday etc FIELD_CAL_RECUR_FILTER_MONTHS = 71, // (int) Bitmask of months: Bit 0 = January, Bit 1 = Feburary etc FIELD_CAL_RECUR_FILTER_YEARS = 72, // (char*) Comma separated list of years to filter on. FIELD_CAL_NOTES = 73, // (char*) Textual notes FIELD_CAL_TYPE = 74, // (CalendarType) The type of event FIELD_CAL_COMPLETED = 75, // (int) 0 -> 100% FIELD_CAL_START_UTC = 76, // (LDateTime*) The start time and date FIELD_CAL_END_UTC = 77, // (LDateTime*) The end time and date FIELD_CAL_RECUR_FILTER_POS = 78, // (char*) Comma separated list of integers defining positions in the month to filter on. FIELD_CAL_RECUR_END_DATE = 79, // (LDateTime*) The end of recurence if FIELD_CAL_RECUR_END_TYPE == CalEndOnDate. FIELD_CAL_RECUR_END_COUNT = 80, // (int) Times to recur if FIELD_CAL_RECUR_END_TYPE == CalEndOnCount. FIELD_CAL_RECUR_END_TYPE = 81, // (CalRecurEndType) Which ending to use... needs an enum FIELD_CAL_RECUR = 82, // (int) true if the event recurs. FIELD_CAL_TIMEZONE = 83, // (char*) The timezone as text FIELD_CAL_PRIVACY = 84, // (CalendarPrivacyType) The privacy setting FIELD_ATTENDEE_JSON = 85, FIELD_CAL_ATTACHMENTS = 86, // List of attached files // 2nd lot of contact fields FIELD_WORK_STREET = 90, // (char*) FIELD_WORK_SUBURB = 91, // (char*) FIELD_WORK_POSTCODE = 92, // (char*) FIELD_WORK_STATE = 93, // (char*) FIELD_WORK_COUNTRY = 94, // (char*) // #define FIELD_WORK_PHONE is previously defined FIELD_WORK_MOBILE = 95, // (char*) FIELD_WORK_IM = 96, // (char*) FIELD_WORK_FAX = 97, // (char*) FIELD_WORK_WEBPAGE = 98, // (char*) FIELD_COMPANY = 99, // (char*) /* Deprecated FIELD_CONTACT_CUST_FLD1 = 100, // (char*) FIELD_CONTACT_CUST_VAL1 = 101, // (char*) FIELD_CONTACT_CUST_FLD2 = 102, // (char*) FIELD_CONTACT_CUST_VAL2 = 103, // (char*) FIELD_CONTACT_CUST_FLD3 = 104, // (char*) FIELD_CONTACT_CUST_VAL3 = 105, // (char*) FIELD_CONTACT_CUST_FLD4 = 106, // (char*) FIELD_CONTACT_CUST_VAL4 = 107, // (char*) */ FIELD_CONTACT_IMAGE = 108, // (LSurface*) FIELD_CONTACT_JSON = 109, // (char*) // Misc additional fields FIELD_LABEL = 110, // (char*) Mail label FIELD_CHARSET = 111, // (char*) A character set FIELD_ALT_EMAIL = 112, // (char*) Comma separated list of alternative, // non-default email addresses. The default addr // is stored under 'FIELD_EMAIL' FIELD_UID = 113, // (char*) FIELD_TITLE = 114, // (char*) FIELD_TIMEZONE = 115, // (char*) FIELD_REFERENCES = 116, // (char*) FIELD_SERVER_UID = 117, // (int64) Server identifier FIELD_FOLDER_PERM_READ = 118, // (int64) FIELD_FOLDER_PERM_WRITE = 119, // (int64) FIELD_GROUP_NAME = 120, // (char*) The name of a contact group FIELD_HTML_CHARSET = 121, // (char*) The character set of the HTML body part FIELD_POSITION = 122, // (char*) Role in organisation FIELD_GROUP_LIST = 123, // (char*) FIELD_FOLDER_THREAD = 124, // (int64) FIELD_ACCOUNT_ID = 125, // (int64) The ID of an account FIELD_FROM_CONTACT_NAME = 126, // (char*) Not used at the Store3 level FIELD_FWD_MSG_ID = 127, // (char*) Mail::FwdMsgId FIELD_FOLDER_TYPE = 128, // (Store3ItemTypes) FIELD_FOLDER_NAME = 129, // (char*) Name of the folder FIELD_UNREAD = 130, // (int64) Count of unread items FIELD_SORT = 131, // (int64) Sort setting FIELD_STATUS = 132, // (Store3Status) FIELD_VERSION = 133, // (int64) FIELD_ID = 134, // (int64) FIELD_READONLY = 135, // (bool) FIELD_NAME = 136, // (char*) FIELD_WIDTH = 137, // (int64) // #define FIELD_ATTACHMENT_CONTENT 139 - Deprecated // #define FIELD_ATTACHMENTS 140 - Deprecated, use FIELD_MIME_SEG instead FIELD_CACHE_FILENAME = 141, // (char*) IMAP backend: the filename FIELD_CACHE_FLAGS = 142, // (char*) IMAP backend: IMAP flags FIELD_DONT_SHOW_PREVIEW = 143, // (bool) FIELD_STORE_PASSWORD = 144, // (char*) FIELD_LOADED = 145, // (Store3State) FIELD_DEBUG = 146, // (char*) FIELD_MIME_SEG = 147, // (LDataIt) FIELD_STORE_TYPE = 148, // (Store3Backend) FIELD_BOUNCE_MSG_ID = 149, // (char*) FIELD_FOLDER_INDEX = 150, // (int64) FIELD_FORMAT = 151, // (int64) FIELD_SYSTEM_FOLDER = 152, // (Store3SystemFolder) FIELD_ERROR = 153, // (char*) An error message FIELD_TYPE = 154, // (char*) The type of the object FIELD_ATTACHMENTS_DATA = 155, // Meta field for specifying attachment data contents FIELD_ATTACHMENTS_NAME = 156, // Meta field for specifying attachment file names FIELD_MEMBER_OF_GROUP = 157, // Meta field for specifying membership of a content group FIELD_TEMP_PATH = 158, // (char*) A temporary path to store files... FIELD_HTML_RELATED = 159, // Array of related attachments for the HTML content. // Pass this to an email's SetObj member to add a // related attachment. FIELD_CAL_REMINDERS = 160, // Individual reminders as CSV, fields are: // Number, CalendarReminderUnits, CalendarReminderType, Param // // CalEmail: param option email address to send to as well as the guests // CalPopup: not used // CalScriptCallback: not impl (but will be the script function and args) // FIELD_CAL_LAST_CHECK = 161, // (LDateTime) Ts the calendar event was last checked for reminders FIELD_DATE_MODIFIED = 162, // (LDateTime) Ts of modification FIELD_INBOX = 163, // (LDataFolderI*) Inbox for mail store FIELD_OUTBOX = 164, // (LDataFolderI*) Outbox for mail store FIELD_SENT = 165, // (LDataFolderI*) Sent folder for mail store FIELD_TRASH = 166, // (LDataFolderI*) Trash folder for mail store FIELD_IMAP_SEQ = 167, // (uint32_t) IMAP sequence number FIELD_CAL_STATUS = 168, // (char*) Status of the vCal event. FIELD_STORE_STATUS = 169, // (ScribeAccountletStatusIcon) Status (icon) of a LDataStoreI FIELD_RECEIVED_DOMAIN = 170, // (char*) First "Received:" header domain. (See also SdReceivedDomain) FIELD_FOLDER_ITEMS = 171, // (int64) Number of items in a folder.. FIELD_PARENT = 172, // (LDataFolderI*) Parent of store3 item + FIELD_URI = 173, // (char*) FIELD_MAX, }; #endif diff --git a/include/lgi/common/vCard-vCal.h b/include/lgi/common/vCard-vCal.h --- a/include/lgi/common/vCard-vCal.h +++ b/include/lgi/common/vCard-vCal.h @@ -1,109 +1,123 @@ #ifndef _VCARD_VCAL_H #define _VCARD_VCAL_H // #include "ScribeDefs.h" #include "Store3.h" #define FIELD_PERMISSIONS 2000 class VIoPriv; class VIo { public: struct Parameter { LString Field; LString Value; void Set(const char *f, const char *v) { Field = f; Value = v; } }; struct TimeZoneSection { int From, To; LDateTime Start; LString Rule; }; struct TimeZoneInfo { LString Name; TimeZoneSection Normal; TimeZoneSection Daylight; }; class ParamArray : public LArray { public: ParamArray(const char *Init = NULL) { if (Init) { LString s = Init; LString::Array a = s.SplitDelimit(","); for (unsigned i=0; i #include #include #include "lgi/common/Lgi.h" #include "lgi/common/vCard-vCal.h" #include "ScribeDefs.h" #include "lgi/common/Json.h" #define DEBUG_LOGGING 0 #define Push(s) Write(s, (int)strlen(s)) #define ClearFields() \ Field.Empty(); \ Params.Empty(); \ Data.Empty() #define IsType(str) (Params.Find(str) != 0) struct TzMap { int Offset; const char *Name; }; #define TZ(h,m) (((h)*100) + (m)) static TzMap Timezones[] = { {TZ(11,0), "Australia/Sydney"}, }; int TimezoneToOffset(const char *Tz) { if (!Tz) return 0; for (int i=0; iName, Tz)) return t->Offset; } return (int)Atoi(Tz); } #if 1 bool IsVar(char *field, const char *s) { if (!s) return false; char *dot = strchr(field, '.'); if (dot) return _stricmp(dot + 1, s) == 0; return _stricmp(field, s) == 0; } #else #define IsVar(field, str) (field != 0 && _stricmp(field, str) == 0) #endif char *DeEscape(char *s, bool QuotedPrintable) { if (!s) return 0; char *i = s; char *o = s; while (*i) { if (*i == '\\') { i++; switch (*i) { case 'n': case 'N': *o++ = '\n'; break; case ',': case ';': case ':': *o++ = *i; break; default: *o++ = '\\'; i--; break; } } else if (QuotedPrintable && *i == '=' && i[1] && i[2]) { i++; char h[3] = { i[0], i[1], 0}; *o++ = htoi(h); i++; } else { *o++ = *i; } i++; } *o = 0; return s; } ///////////////////////////////////////////////////////////// // General IO class class VIoPriv { public: LStringPipe Buf; }; VIo::VIo() : d(new VIoPriv) { } VIo::~VIo() { DeleteObj(d); } bool VIo::ParseDate(LDateTime &Out, char *In) { bool Status = false; if (In) { Out.SetTimeZone(0, false); auto v = LString(In).SplitDelimit("T"); if (v.Length() > 0) { if (v[0].Length() == 8) { auto Year = v[0](0, 4); auto Month = v[0](4, 6); auto Day = v[0](6, 8); Out.Year ((int)Year.Int()); Out.Month((int)Month.Int()); Out.Day ((int)Day.Int()); Status = true; } char *t = v[1]; if (t && strlen(t) >= 6) { char Hour[3] = {t[0], t[1], 0}; char Minute[3] = {t[2], t[3], 0}; char Second[3] = {t[4], t[5], 0}; Out.Hours(atoi(Hour)); Out.Minutes(atoi(Minute)); Out.Seconds(atoi(Second)); Status = true; } } } return Status; } bool VIo::ParseDuration(LDateTime &Out, int &Sign, char *In) { bool Status = false; if (In) { Sign = 1; if (*In == '-') { Sign = -1; In++; } if (toupper(*In++) == 'P' && toupper(*In++) == 'T') { while (*In) { int i = atoi(In); while (IsDigit(*In)) In++; switch (toupper(*In++)) { case 'W': { Out.Day(Out.Day() + (i * 7)); break; } case 'D': { Out.Day(Out.Day() + i); break; } case 'H': { Out.Hours(Out.Hours() + i); break; } case 'M': { Out.Minutes(Out.Minutes() + i); break; } case 'S': { Out.Seconds(Out.Seconds() + i); break; } } } Status = true; } } return Status; } void VIo::Fold(LStreamI &o, const char *i, int pre_chars) { int x = pre_chars; for (const char *s=i; s && *s;) { if (x >= 74) { // wrapping o.Write(i, (int)(s-i)); o.Write((char*)"\r\n\t", 3); x = 0; i = s; } else if (*s == '=' || ((((uint8_t)*s) & 0x80) != 0)) { // quoted printable o.Write(i, (int)(s-i)); LStreamPrint(&o, "=%02.2x", (uint8_t)*s); x += 3; i = ++s; } else if (*s == '\n') { // new line o.Write(i, (int)(s-i)); o.Write((char*)"\\n", 2); x += 2; i = ++s; } else if (*s == '\r') { o.Write(i, (int)(s-i)); i = ++s; } else { s++; x++; } } o.Write(i, (int)strlen(i)); } char *VIo::Unfold(char *In) { if (In) { LStringPipe p(256); for (char *i=In; i && *i; i++) { if (*i == '\n') { if (i[1] && strchr(" \t", i[1])) { i++; } else { p.Write(i, 1); } } else { p.Write(i, 1); } } return p.NewStr(); } return 0; } char *VIo::UnMultiLine(char *In) { if (In) { LStringPipe p; char *n; for (char *i=In; i && *i; i=n) { n = stristr(i, "\\n"); if (n) { p.Write(i, (int)(n-i)); p.Push((char*)"\n"); n += 2; } else { p.Push(i); } } return p.NewStr(); } return 0; } ///////////////////////////////////////////////////////////// // VCard class bool VCard::Import(LDataPropI *c, LStreamI *s) { bool Status = false; if (!c || !s) return false; LString Field; ParamArray Params; LString Data; ssize_t PrefEmail = -1; LString::Array Emails; while (ReadField(*s, Field, &Params, Data)) { if (_stricmp(Field, "begin") == 0 && _stricmp(Data, "vcard") == 0) { while (ReadField(*s, Field, &Params, Data)) { if (_stricmp(Field, "end") == 0 && _stricmp(Data, "vcard") == 0) goto ExitLoop; if (IsVar(Field, "n")) { auto Name = Data.SplitDelimit(";", -1, false); char *First = Name[1]; char *Last = Name[0]; char *Title = Name[3]; if (First) { c->SetStr(FIELD_FIRST_NAME, First); Status = true; } if (Last) { c->SetStr(FIELD_LAST_NAME, Last); Status = true; } if (Title) { c->SetStr(FIELD_TITLE, Title); Status = true; } } else if (IsVar(Field, "nickname")) { c->SetStr(FIELD_NICK, Data); } else if (IsVar(Field, "tel")) { auto Phone = Data.SplitDelimit(";", -1, false); for (uint32_t p=0; pSetStr(FIELD_WORK_MOBILE, Phone[p]); } else { c->SetStr(FIELD_HOME_MOBILE, Phone[p]); } } else if (IsType("fax")) { if (IsType("Work")) { c->SetStr(FIELD_WORK_FAX, Phone[p]); } else { c->SetStr(FIELD_HOME_FAX, Phone[p]); } } else { if (IsType("Work")) { c->SetStr(FIELD_WORK_PHONE, Phone[p]); } else { c->SetStr(FIELD_HOME_PHONE, Phone[p]); } } } } else if (IsVar(Field, "email")) { if (IsType("pref")) { PrefEmail = Emails.Length(); } Emails.Add(Data); } else if (IsVar(Field, "org")) { auto Org = Data.SplitDelimit(";", -1, false); if (Org[0]) c->SetStr(FIELD_COMPANY, Org[0]); } else if (IsVar(Field, "adr")) { bool IsWork = IsType("work"); // bool IsHome = IsType("home"); auto Addr = Data.SplitDelimit(";", -1, false); if (Addr[2]) { auto A = Addr[2].SplitDelimit("\r\n"); if (A.Length() > 1) { c->SetStr(IsWork ? FIELD_WORK_STREET : FIELD_HOME_STREET, A[0]); if (A[1]) c->SetStr(IsWork ? FIELD_WORK_SUBURB : FIELD_HOME_SUBURB, A[1]); if (A[2]) c->SetStr(IsWork ? FIELD_WORK_COUNTRY : FIELD_HOME_COUNTRY, A[2]); } else { c->SetStr(IsWork ? FIELD_WORK_STREET : FIELD_HOME_STREET, Addr[2]); } } if (Addr[3]) c->SetStr(IsWork ? FIELD_WORK_SUBURB : FIELD_HOME_SUBURB, Addr[3]); if (Addr[4]) c->SetStr(IsWork ? FIELD_WORK_STATE : FIELD_HOME_STATE, Addr[4]); if (Addr[5]) c->SetStr(IsWork ? FIELD_WORK_POSTCODE : FIELD_HOME_POSTCODE, Addr[5]); if (Addr[6]) c->SetStr(IsWork ? FIELD_WORK_COUNTRY : FIELD_HOME_COUNTRY, Addr[6]); } else if (IsVar(Field, "note")) { c->SetStr(FIELD_NOTE, Data); } else if (IsVar(Field, "uid")) { auto n = Data.SplitDelimit(";", -1, false); c->SetStr(FIELD_UID, n[0]); } else if (IsVar(Field, "x-perm")) { int Perms = atoi(Data); c->SetInt(FIELD_PERMISSIONS, Perms); } else if (IsVar(Field, "url")) { bool IsWork = IsType("work"); bool IsHome = IsType("home"); if (IsWork) { c->SetStr(FIELD_HOME_WEBPAGE, Data); } else if (IsHome) { c->SetStr(FIELD_WORK_WEBPAGE, Data); } } else if (IsVar(Field, "nickname")) { c->SetStr(FIELD_NICK, Data); } else if (IsVar(Field, "photo")) { size_t B64Len = strlen(Data); ssize_t BinLen = BufferLen_64ToBin(B64Len); LAutoPtr Bin(new uint8_t[BinLen]); if (Bin) { ssize_t Bytes = ConvertBase64ToBinary(Bin.Get(), BinLen, Data, B64Len); LVariant v; if (v.SetBinary(Bytes, Bin.Release(), true)) { c->SetVar(FIELD_CONTACT_IMAGE, &v); } } } } } } ExitLoop: if (Emails.Length()) { if (PrefEmail < 0) PrefEmail = 0; c->SetStr(FIELD_EMAIL, Emails[PrefEmail]); Emails.DeleteAt(PrefEmail); if (Emails.Length()) { LStringPipe p; for (uint32_t i=0; iSetStr(FIELD_ALT_EMAIL, v); DeleteArray(v); } } } ClearFields(); return Status; } bool VIo::ReadField(LStreamI &s, LString &Name, ParamArray *Params, LString &Data) { bool Status = false; ParamArray LocalParams; Name.Empty(); Data.Empty(); if (Params) Params->Empty(); else Params = &LocalParams; char Temp[1024]; LArray p; bool Done = false; while (!Done) { bool EatNext = false; ReadNextLine: Temp[0] = 0; int64 r = d->Buf.Pop(Temp, sizeof(Temp)); if (r <= 0) { // Try reading more data... r = s.Read(Temp, sizeof(Temp)); if (r > 0) d->Buf.Write(Temp, r); else break; r = d->Buf.Pop(Temp, sizeof(Temp)); } else if (Temp[0] == '\r' || Temp[1] == '\n') { // Skip empty lines... goto ReadNextLine; } if (r <= 0) break; if ((r == 1 && Temp[0] == '\n') || (r == 2 && Temp[0] == '\r' && Temp[1] == '\n')) // Blank line case... continue; // Unfold for (char *c = Temp; *c; c++) { if (*c == '\r') { // do nothing } else if (*c == '\n') { char Next; r = d->Buf.Peek((uchar*) &Next, 1); if (r == 0) { r = s.Read(Temp, sizeof(Temp)); if (r <= 0) break; d->Buf.Write(Temp, (int)r); r = d->Buf.Peek((uchar*) &Next, 1); } if (r == 1) { if (Next == ' ' || Next == '\t') { // Wrapped, do nothing EatNext = true; goto ReadNextLine; } else { Done = true; break; } } else { break; } } else if (EatNext) { EatNext = false; } else { p.Add(*c); } } } p.Add(0); char *f = p.Length() > 1 ? &p[0] : 0; if (f) { char *e = strchr(f, ':'); if (e) { *e++ = 0; auto t = LString(f).SplitDelimit(";"); if (t.Length() > 0) { Name = t[0]; for (uint32_t i=1; iFind("charset"); if (Charset) { LAutoString u((char*)LNewConvertCp("utf-8", e, Charset)); Data = u.Get(); } else { Data = e; } Status = Name.Length() > 0; } return Status; } void VIo::WriteField(LStreamI &s, const char *Name, ParamArray *Params, const char *Data) { if (Name && Data) { int64 Size = s.GetSize(); LStreamPrint(&s, "%s", Name); if (Params) { for (uint32_t i=0; iLength(); i++) { Parameter &p = (*Params)[i]; LStreamPrint(&s, "%s%s=%s", i?"":";", p.Field.Get(), p.Value.Get()); } } bool Is8Bit = false; bool HasEq = false; for (uint8_t *c = (uint8_t*)Data; *c; c++) { if ((*c & 0x80) != 0) { Is8Bit = true; } else if (*c == '=') { HasEq = true; } } if (Is8Bit || HasEq) { if (Is8Bit) s.Write((char*)";charset=utf-8", 14); s.Write((char*)";encoding=quoted-printable", 26); } s.Write((char*)":", 1); Fold(s, Data, (int) (s.GetSize() - Size)); s.Write((char*)"\r\n", 2); } } bool VCard::Export(LDataPropI *c, LStreamI *o) { if (!c || !o) return false; bool Status = true; char s[512]; const char *Empty = ""; o->Push("begin:vcard\r\n"); o->Push("version:3.0\r\n"); const char *First = 0, *Last = 0, *Title = 0; First = c->GetStr(FIELD_FIRST_NAME); Last = c->GetStr(FIELD_LAST_NAME); Title = c->GetStr(FIELD_TITLE); if (First || Last) { sprintf_s(s, sizeof(s), "%s;%s;%s", Last?Last:Empty, First?First:Empty, Title?Title:Empty); WriteField(*o, "n", 0, s); } #define OutputTypedField(Field, Name, Type) \ { const char *str = 0; \ if ((str = c->GetStr(Name))) \ { \ WriteField(*o, Field, Type, str); \ } } #define OutputField(Field, Name) \ { const char *str = 0; \ if ((str = c->GetStr(Name))) \ { \ WriteField(*o, Field, 0, str); \ } } ParamArray Work("Work"), WorkCell("Work,Cell"), WorkFax("Work,Fax"); ParamArray Home("Home"), HomeCell("Home,Cell"), HomeFax("Home,Fax"); ParamArray InetPref("internet,pref"), Inet("internet"); OutputTypedField("tel", FIELD_WORK_PHONE, &Work); OutputTypedField("tel", FIELD_WORK_MOBILE, &WorkCell); OutputTypedField("tel", FIELD_WORK_FAX, &WorkFax); OutputTypedField("tel", FIELD_HOME_PHONE, &Home); OutputTypedField("tel", FIELD_HOME_MOBILE, &HomeCell); OutputTypedField("tel", FIELD_HOME_FAX, &HomeFax); OutputField("org", FIELD_COMPANY); OutputTypedField("email", FIELD_EMAIL, &InetPref); const char *Alt; if ((Alt = c->GetStr(FIELD_ALT_EMAIL))) { auto t = LString(Alt).SplitDelimit(","); for (unsigned i=0; iGetStr(FIELD_UID))) { LStreamPrint(o, "UID:%s\r\n", Uid); } const char *Street, *Suburb, *PostCode, *State, *Country; Street = Suburb = PostCode = State = Country = 0; Street = c->GetStr(FIELD_HOME_STREET); Suburb = c->GetStr(FIELD_HOME_SUBURB); PostCode = c->GetStr(FIELD_HOME_POSTCODE); State = c->GetStr(FIELD_HOME_STATE); Country = c->GetStr(FIELD_HOME_COUNTRY); if (Street || Suburb || PostCode || State || Country) { sprintf_s(s, sizeof(s), ";;%s;%s;%s;%s;%s", Street?Street:Empty, Suburb?Suburb:Empty, State?State:Empty, PostCode?PostCode:Empty, Country?Country:Empty); WriteField(*o, "adr", &Home, s); } Street = Suburb = PostCode = State = Country = 0; Street = c->GetStr(FIELD_WORK_STREET); Suburb = c->GetStr(FIELD_WORK_SUBURB); PostCode = c->GetStr(FIELD_WORK_POSTCODE); State = c->GetStr(FIELD_WORK_STATE); Country = c->GetStr(FIELD_WORK_COUNTRY); if (Street || Suburb || PostCode || State || Country) { sprintf_s(s, sizeof(s), ";;%s;%s;%s;%s;%s", Street?Street:Empty, Suburb?Suburb:Empty, State?State:Empty, PostCode?PostCode:Empty, Country?Country:Empty); WriteField(*o, "adr", &Work, s); } // OutputField("X-Perm", FIELD_PERMISSIONS); const char *Url; if ((Url = c->GetStr(FIELD_HOME_WEBPAGE))) { WriteField(*o, "url", &Home, Url); } if ((Url = c->GetStr(FIELD_WORK_WEBPAGE))) { WriteField(*o, "url", &Work, Url); } const char *Nick; if ((Nick = c->GetStr(FIELD_NICK))) { WriteField(*o, "nickname", 0, Nick); } const char *Note; if ((Note = c->GetStr(FIELD_NOTE))) { WriteField(*o, "note", 0, Note); } const LVariant *Photo = c->GetVar(FIELD_CONTACT_IMAGE); if (Photo && Photo->Type == GV_BINARY) { ssize_t B64Len = BufferLen_BinTo64(Photo->Value.Binary.Length); LAutoPtr B64Buf(new char[B64Len]); if (B64Buf) { ssize_t Bytes = ConvertBinaryToBase64(B64Buf, B64Len, (uchar*)Photo->Value.Binary.Data, Photo->Value.Binary.Length); if (Bytes > 0) { LStreamPrint(o, "photo;type=jpeg;encoding=base64:\r\n"); int LineChar = 76; for (ssize_t i=0; i LineChar ? LineChar : Remain; o->Write(" ", 1); o->Write(B64Buf + i, Wr); o->Write("\r\n", 2); i += Wr; } } } } o->Push("end:vcard\r\n"); return Status; } ///////////////////////////////////////////////////////////// // VCal class int StringToWeekDay(const char *s) { const char *days[] = {"SU","MO","TU","WE","TH","FR","SA"}; for (unsigned i=0; i TzInfos; TimeZoneInfo *TzInfo = NULL; bool IsNormalTz = false, IsDaylightTz = false; LJson To; int Attendee = 0; LArray Alarms; int AlarmIdx = -1; while (ReadField(*In, Field, &Params, Data)) { if (!_stricmp(Field, "begin")) { if (_stricmp(Data, "vevent") == 0 || _stricmp(Data, "vtodo") == 0) { IsEvent = true; SectionType = Data; int Type = _stricmp(Data, "vtodo") == 0 ? 1 : 0; c->SetInt(FIELD_CAL_TYPE, Type); } else if (!_stricmp(Data, "vtimezone")) { IsTimeZone = true; TzInfo = &TzInfos.New(); } else if (_stricmp(Data, "vcalendar") == 0) IsCal = true; else if (!_stricmp(Data, "standard")) IsNormalTz = true; else if (!_stricmp(Data, "daylight")) IsDaylightTz = true; else if (!_stricmp(Data, "valarm")) { IsAlarm = true; AlarmIdx++; } } else if (_stricmp(Field, "end") == 0) { if (_stricmp(Data, "vcalendar") == 0) { IsCal = false; } else if (SectionType && _stricmp(Data, SectionType) == 0) { Status = true; IsEvent = false; break; // exit loop } else if (!_stricmp(Data, "vtimezone")) { IsTimeZone = false; TzInfo = NULL; } else if (!_stricmp(Data, "standard")) IsNormalTz = false; else if (!_stricmp(Data, "daylight")) IsDaylightTz = false; else if (!_stricmp(Data, "valarm")) IsAlarm = false; } else if (IsEvent) { if (IsAlarm) { auto &a = Alarms[AlarmIdx]; if (IsVar(Field, "ACTION")) a.Action = Data; else if (IsVar(Field, "DESCRIPTION")) a.Desc = Data; else if (IsVar(Field, "TRIGGER")) a.Trigger = Data; else if (IsVar(Field, "X-PARAM")) a.Param = Data; } else if (IsVar(Field, "dtstart")) { ParseDate(EventStart, Data); StartTz = Params.Find("TZID"); } else if (IsVar(Field, "dtend")) { ParseDate(EventEnd, Data); EndTz = Params.Find("TZID"); } else if (IsVar(Field, "summary")) { c->SetStr(FIELD_CAL_SUBJECT, Data); } else if (IsVar(Field, "description")) { LAutoString Sum(UnMultiLine(Data)); if (Sum) c->SetStr(FIELD_CAL_NOTES, Sum); } else if (IsVar(Field, "location")) { c->SetStr(FIELD_CAL_LOCATION, Data); } else if (IsVar(Field, "uid")) { char *Uid = Data; c->SetStr(FIELD_UID, Uid); } else if (IsVar(Field, "x-showas")) { char *n = Data; if (Stricmp(n, "TENTATIVE") == 0) c->SetInt(FIELD_CAL_SHOW_TIME_AS, CalTentative); else if (Stricmp(n, "BUSY") == 0) c->SetInt(FIELD_CAL_SHOW_TIME_AS, CalBusy); else if (Stricmp(n, "OUT") == 0) c->SetInt(FIELD_CAL_SHOW_TIME_AS, CalOut); else c->SetInt(FIELD_CAL_SHOW_TIME_AS, CalFree); } else if (IsVar(Field, "attendee")) { auto Email = Data.SplitDelimit(":=").Last(); if (LIsValidEmail(Email)) { - const char *Name = Params.Find("CN"); - const char *Role = Params.Find("Role"); + auto Name = Params.Find("CN"); + auto Role = Params.Find("Role"); LString k; k.Printf("[%i].email", Attendee); To.Set(k, Email); if (Name) { k.Printf("[%i].name", Attendee); To.Set(k, Name); } if (Role) { k.Printf("[%i].role", Attendee); To.Set(k, Role); } Attendee++; } } else if (IsVar(Field, "status")) { c->SetStr(FIELD_CAL_STATUS, Data); } + else if (IsVar(Field, "attach")) + { + // None of these error cases are fatal, the backend may not support + // attachments... but log something for the user in that case. + auto attachments = c->GetList(FIELD_CAL_ATTACHMENTS); + if (!attachments) + { + LgiTrace("%s:%i - vCal import: No attachment list...\n", _FL); + } + else if (auto data = dynamic_cast(c)) + { + auto store = data->GetStore(); + if (!store) + { + LgiTrace("%s:%i - vCal import: No store object.\n", _FL); + } + // Create a new attachment object to receive the file + else if (auto file = attachments->Create(store)) + { + auto FileName = Params.Find("X-FILENAME"); + auto MimeType = Params.Find("FMTTYPE"); + auto Encoding = Params.Find("ENCODING"); + auto Value = Params.Find("VALUE"); + + if (FileName) + file->SetStr(FIELD_NAME, FileName); + if (MimeType) + file->SetStr(FIELD_MIME_TYPE, MimeType); + + if (!Stricmp(Encoding, "base64")) + { + auto bin = LToBinary(Data); + file->SetStr(FIELD_ATTACHMENTS_DATA, bin); + } + else if (Data.Find("://") >= 0) + { + file->SetStr(FIELD_URI, Data); + } + else + { + LgiTrace("%s:%i - vCal import: Unknown data type?\n", _FL); + LAssert(!"What is 'Data'?"); + } + + if (auto fileData = dynamic_cast(file)) + fileData->Save(data); + } + else + { + LgiTrace("%s:%i - vCal import: Couldn't create attachment object\n", _FL); + } + } + else + { + LgiTrace("%s:%i - vCal import: no LDataI object\n", _FL); + } + } } else if (IsTimeZone && TzInfo) { /* e.g.: TZID:Pacific Standard Time BEGIN:STANDARD DTSTART:16010101T020000 TZOFFSETFROM:-0700 TZOFFSETTO:-0800 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11 END:STANDARD BEGIN:DAYLIGHT DTSTART:16010101T020000 TZOFFSETFROM:-0800 TZOFFSETTO:-0700 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3 END:DAYLIGHT */ if (IsVar(Field, "TZID")) { TzInfo->Name = Data; } else if (IsNormalTz || IsDaylightTz) { TimeZoneSection &Sect = IsNormalTz ? TzInfo->Normal : TzInfo->Daylight; if (IsVar(Field, "DTSTART")) ParseDate(Sect.Start, Data); else if (IsVar(Field, "TZOFFSETFROM")) Sect.From = (int)Data.Int(); else if (IsVar(Field, "TZOFFSETTO")) Sect.To = (int)Data.Int(); else if (IsVar(Field, "RRULE")) Sect.Rule = Data; } } } if (Attendee > 0) { auto j = To.GetJson(); c->SetStr(FIELD_ATTENDEE_JSON, j); } if (StartTz || EndTz) { // Did we get a timezone defn? TimeZoneInfo *Match = NULL; for (unsigned i=0; iNormal, EventStart.Year()) && EvalRule(Dst, Match->Daylight, EventStart.Year())) { bool IsDst = false; if (Dst < Norm) { // DST in the middle of the year // |Jan----DST------Norm----Dec| if (EventStart >= Dst && EventStart <= Norm) IsDst = true; } else { // DST over the start and end of the year // |Jan----Norm------DST----Dec| if (EventStart >= Norm && EventStart <= Dst) IsDst = false; else IsDst = true; } #if DEBUG_LOGGING LgiTrace("Eval Start=%s, Norm=%s, Dst=%s, IsDst=%i\n", EventStart.Get().Get(), Norm.Get().Get(), Dst.Get().Get(), IsDst); #endif EffectiveTz = IsDst ? Match->Daylight.To : Match->Normal.To; LString sTz; sTz.Printf("%4.4i,%s", EffectiveTz, StartTz.Get()); c->SetStr(FIELD_CAL_TIMEZONE, sTz); } else goto StoreStringTz; } else { // Store whatever they gave us StoreStringTz: if (StartTz.Equals(EndTz)) c->SetStr(FIELD_CAL_TIMEZONE, StartTz); else if (StartTz.Get() && EndTz.Get()) { LString s; s.Printf("%s,%s", StartTz.Get(), EndTz.Get()); c->SetStr(FIELD_CAL_TIMEZONE, s); } else if (StartTz) c->SetStr(FIELD_CAL_TIMEZONE, StartTz); else if (EndTz) c->SetStr(FIELD_CAL_TIMEZONE, EndTz); if (StartTz) EffectiveTz = TimezoneToOffset(StartTz); } if (EffectiveTz) { // Convert the event to UTC int e = abs(EffectiveTz); int Mins = (((e / 100) * 60) + (e % 100)) * (EffectiveTz < 0 ? -1 : 1); #if DEBUG_LOGGING LgiTrace("%s:%i - EffectiveTz=%i, Mins=%i\n", _FL, EffectiveTz, Mins); #endif if (EventStart.IsValid()) { #if DEBUG_LOGGING LgiTrace("EventStart=%s\n", EventStart.Get().Get()); #endif EventStart.AddMinutes(-Mins); #if DEBUG_LOGGING LgiTrace("EventStart=%s\n", EventStart.Get().Get()); #endif } if (EventEnd.IsValid()) EventEnd.AddMinutes(-Mins); } } if (EventStart.IsValid()) c->SetDate(FIELD_CAL_START_UTC, &EventStart); if (EventEnd.IsValid()) c->SetDate(FIELD_CAL_END_UTC, &EventEnd); if (Alarms.Length()) { LString::Array s; for (LAlarm &a: Alarms) { LString r = a.GetStr(); if (r) s.Add(r); } if (s.Length()) { c->SetStr(FIELD_CAL_REMINDERS, LString("\n").Join(s)); } } ClearFields(); return Status; } LString ToString(LDateTime &dt) { LString s; s.Printf("%04.4i%02.2i%02.2iT%02.2i%02.2i%02.2i%s", dt.Year(), dt.Month(), dt.Day(), dt.Hours(), dt.Minutes(), dt.Seconds(), dt.GetTimeZone() ? "" : "Z"); return s; } bool VCal::Export(LDataPropI *c, LStreamI *o) { if (!c || !o) return false; int64 Type = c->GetInt(FIELD_CAL_TYPE); // auto *TimeZone = c->GetStr(FIELD_CAL_TIMEZONE); char *TypeStr = Type == 1 ? (char*)"VTODO" : (char*)"VEVENT"; o->Push((char*)"BEGIN:VCALENDAR\r\n" "VERSION:2.0\r\n" "CALSCALE:GREGORIAN\r\n" "PRODID:Memecode vcal filter\r\n"); if (c) { LStreamPrint(o, "BEGIN:%s\r\n", TypeStr); #define OutputStr(Field, Name) \ { \ const char *_s = 0; \ if ((_s = c->GetStr(Field))) \ { \ WriteField(*o, Name, 0, _s); \ } \ } LDateTime Now; Now.SetNow(); Now.ToUtc(); LStreamPrint(o, "DTSTAMP:%s\r\n", ToString(Now).Get()); OutputStr(FIELD_CAL_SUBJECT, "SUMMARY"); OutputStr(FIELD_CAL_LOCATION, "LOCATION"); OutputStr(FIELD_CAL_NOTES, "DESCRIPTION"); OutputStr(FIELD_CAL_STATUS, "STATUS"); int64 ShowAs; if ((ShowAs = c->GetInt(FIELD_CAL_SHOW_TIME_AS))) { switch (ShowAs) { default: case CalFree: o->Push((char*)"X-SHOWAS:FREE\r\n"); break; case CalTentative: o->Push((char*)"X-SHOWAS:TENTATIVE\r\n"); break; case CalBusy: o->Push((char*)"X-SHOWAS:BUSY\r\n"); break; case CalOut: o->Push((char*)"X-SHOWAS:OUT\r\n"); break; } } const char *Uid; if ((Uid = c->GetStr(FIELD_UID))) { LStreamPrint(o, "UID:%s\r\n", Uid); } const LDateTime *Dt; if ((Dt = c->GetDate(FIELD_CAL_START_UTC))) { LDateTime dt = *Dt; dt.ToUtc(); LStreamPrint(o, "DTSTART:%s\r\n", ToString(dt).Get()); } if ((Dt = c->GetDate(FIELD_CAL_END_UTC))) { LDateTime dt = *Dt; dt.ToUtc(); LStreamPrint(o, "DTEND:%s\r\n", ToString(dt).Get()); } + + if (auto attachments = c->GetList(FIELD_CAL_ATTACHMENTS)) + { + // Save the attachments to 'ATTACH' fields + for (auto file = attachments->First(); file; file = attachments->Next()) + { + auto fileData = dynamic_cast(file); + auto uri = file->GetStr(FIELD_URI); + auto fileName = file->GetStr(FIELD_NAME); + auto mimeType = file->GetStr(FIELD_MIME_TYPE); + + ParamArray param; + if (fileName) + param.Set("X-FILENAME", fileName); + if (mimeType) + param.Set("FMTTYPE", mimeType); + + if (uri) + { + // Save as a URI + WriteField(*o, "ATTACH", ¶m, uri); + } + else if (!fileData) + { + LAssert(!"Should be a LDataI object."); + } + else + { + auto stream = fileData->GetStream(_FL); + if (!stream) + LAssert(!"Should have either URI or Stream"); + else + { + // Base64 encode the stream + LString bin; + if (!bin.Length(stream->GetSize())) + LAssert(!"Failed to set string size"); + else + { + stream->Read(bin.Get(), bin.Length()); + auto b64 = LToBase64(bin); + if (!b64) + LAssert(!"Failed to convert to base64"); + else + { + param.Set("ENCODING", "BASE64"); + param.Set("VALUE", "BINARY"); + WriteField(*o, "ATTACH", ¶m, b64); + } + } + } + } + } + } const char *Reminders; if ((Reminders = c->GetStr(FIELD_CAL_REMINDERS))) { auto Lines = LString(Reminders).SplitDelimit("\n"); for (auto Ln: Lines) { // Fields are: Number, CalendarReminderUnits, CalendarReminderType, Param auto p = Ln.SplitDelimit(","); if (p.Length() >= 3) { const char *Duration = NULL; int64 Count = p[0].Int(); switch (p[1].Int()) { case CalMinutes: Duration = "M"; break; case CalHours: Duration = "H"; break; case CalDays: Duration = "D"; break; case CalWeeks: Duration = "D"; Count *= 7; break; } const char *Action = NULL; switch (p[2].Int()) { case CalEmail: Action = "EMAIL"; break; case CalPopup: Action = "DISPLAY"; break; case CalScriptCallback: Action = "X-SCRIPT"; break; } if (Action && Duration) { LString s; s.Printf( "BEGIN:VALARM\r\n" "ACTION:%s\r\n" "DESCRIPTION:REMINDER\r\n" "TRIGGER:-PT" LPrintfInt64 "%s\r\n", Action, Count, Duration ); if (p.Length() > 3) s += LString("X-PARAM: ") + p[3]; s += "END:VALARM\r\n"; o->Write(s.Get(), s.Length()); } else LAssert(0); } } } LJson j(c->GetStr(FIELD_ATTENDEE_JSON)); for (auto g: j.GetArray(LString())) { // e.g.: ATTENDEE;CN="Matthew Allen";ROLE=REQ-PARTICIPANT;RSVP=TRUE:MAILTO:matthew@company.com.au auto Email = g.Get("email"); if (Email) { auto Name = g.Get("name"); auto Role = g.Get("role"); char s[400]; int ch = sprintf_s(s, sizeof(s), "ATTENDEE"); if (Name) ch += sprintf_s(s+ch, sizeof(s)-ch, ";CN=\"%s\"", Name.Get()); if (Role) ch += sprintf_s(s+ch, sizeof(s)-ch, ";ROLE=\"%s\"", Role.Get()); ch += sprintf_s(s+ch, sizeof(s)-ch, ":MAILTO=%s\r\n", Email.Get()); o->Write(s, ch); } } LStreamPrint(o, "END:%s\r\n", TypeStr); } o->Push((char*)"END:VCALENDAR\r\n"); return true; }