diff --git a/src/common/Text/vCard-vCal.cpp b/src/common/Text/vCard-vCal.cpp --- a/src/common/Text/vCard-vCal.cpp +++ b/src/common/Text/vCard-vCal.cpp @@ -1,1658 +1,1660 @@ #include #include #include #include "lgi/common/Lgi.h" #include "lgi/common/vCard-vCal.h" #include "lgi/common/Json.h" #include "lgi/common/TextConvert.h" #include "ScribeDefs.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, const char *encoding) { bool base64 = !Stricmp(encoding, "base64"); bool quotePrintable = !base64; 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 (quotePrintable && (*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) { LAssert(!"Invalid params"); return; } int64 Size = s.GetSize(); LStreamPrint(&s, "%s", Name); const char *encoding = NULL; const char *charset = NULL; if (Params) { for (auto &p: *Params) LStreamPrint(&s, ";%s=%s", p.Field.Get(), p.Value.Get()); encoding = Params->Find("ENCODING"); charset = Params->Find("CHARSET"); } size_t Is7Bit = 0; size_t Is8Bit = 0; size_t HasEq = 0; for (uint8_t *c = (uint8_t*)Data; *c; c++) { if ((*c & 0x80) != 0) Is8Bit++; else if (*c == '=') HasEq++; else Is7Bit++; } size_t DataLen = Is7Bit + Is8Bit + HasEq; if (Is8Bit || HasEq) { if (!encoding) { encoding = "quoted-printable"; LStreamPrint(&s, ";ENCODING=%s", encoding); } if (Is8Bit && !charset) LStreamPrint(&s, ";charset=utf-8"); } s.Write((char*)":", 1); LString encoded; if (encoding && !Stricmp(encoding, "base64")) { encoded = LToBase64(Data, DataLen); Data = encoded.Get(); } Fold(s, Data, (int) (s.GetSize() - Size), encoding); 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)) { 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"); auto Now = LDateTime::Now(); if (FileName) file->SetStr(FIELD_NAME, FileName); if (MimeType) file->SetStr(FIELD_MIME_TYPE, MimeType); file->SetDate(FIELD_DATE_MODIFIED, &Now); 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))) + if ((Dt = c->GetDate(FIELD_CAL_START_UTC)) && + Dt->IsValid()) { LDateTime dt = *Dt; dt.ToUtc(); LStreamPrint(o, "DTSTART:%s\r\n", ToString(dt).Get()); } - if ((Dt = c->GetDate(FIELD_CAL_END_UTC))) + if ((Dt = c->GetDate(FIELD_CAL_END_UTC)) && + Dt->IsValid()) { 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; }