Class CaldavInvitationLandingService

java.lang.Object
org.exoplatform.caldav.service.CaldavInvitationLandingService

@Service public class CaldavInvitationLandingService extends Object
Lands an invitation a user received by mail in their calendar (EXO-90848): the event the mail describes ends up on the user's CalDAV account and in the eXo calendar standing for it, under the identity the inbound sweep knows it by — with the answer the user gave, or without one when they only asked to add it — and leaves it when its organiser cancelled it.

The sweep's own path, never a second one

The eXo event is created by CaldavInboundService.importInto(long, java.lang.String, org.exoplatform.caldav.model.CalendarSync, org.exoplatform.agenda.model.Calendar, java.time.Instant, java.time.Instant) reading the collection around the event, exactly as the sweep would on its next pass: the same mapping row (ObjectSync: binding, UID, href, ETag), the same remote identity agenda records, the same recurrence and override handling. The read comes first: a mail server that files invitations in the calendar itself (BlueMind, Google) already holds the object, under whatever name it chose for it, and the import finds it by the UID inside it. Only an object the import did not find is written, under a name of this service's own, created and never overwritten; then read back the same way. What this service adds is what the sweep cannot know — what the user asked.

What each click does

An invitation accepted or tentatively accepted, or added without an answer, lands: created, or the copy the user holds rewritten by its organiser's newer revision. Added without an answer, the copy's line for the user says NEEDS-ACTION, or keeps the answer the copy they held carried: the PARTSTAT the sender wrote on that line is the sender's word, and held(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String) reads the stored line back as the user's answer. A decline creates nothing — a meeting the user turned down and never added does not belong in their calendar — and sets the answer on a copy they do hold, found by the same read first (a copy the mail server filed and the sweep has not mapped yet takes the decline too, or the next sweep would bring it in accepted), as answering Decline in agenda on a synced copy does; the copy stays, declined, which is what the server and every client of it then agree on. A cancellation removes the copy the user holds — the object on the server, the mapping, the eXo event — as every calendar client does with the organiser's CANCEL; a cancellation of an event the user never added does nothing.

Where the answer lives

The answer is written onto the copy here (pushAnswerOnto, offered every address the copy may name the user by, their mailbox first), and nowhere else: the import makes the user the creator of the event it brings in, and this add-on's EventResponseSavedListener, which carries eXo answers onto copies through CaldavPushService#pushAnswer, declines every event its user organises — so it never reaches a landed one. The write is conditioned on the version the server holds now, read on this very call, not on the one the last sweep recorded: a mail server that filed the organiser's update itself, or a phone that touched the copy, moved it since, and the sweep leaves a version unrecorded on purpose when eXo's copy is the newer one. The answer touches one attendee line and nothing else, which is what makes that safe. Then it is recorded in agenda like any answer given in eXo, through AgendaEventAttendeeService.sendEventResponse(long, long, org.exoplatform.agenda.constant.EventAttendeeResponse). A copy naming the user only by an address outside the offered set is stored as sent and the answer lives in agenda alone — said at WARN, since nothing retries it.

What is trusted

The object is the sender's. Only the answering user's own bindings are consulted, never the mirror ledger; a UID this deployment minted for one of its own meetings is refused before anything is written, and the UID agenda mails for one of its own meetings (agenda-event-<id>@<this host>) lands nothing: the meeting is in agenda already, so adding it hands back agenda's page as "already in your calendar" when the user may read it and the message describes it, and an answer or a cancellation carried by such a message is refused. Only an invitation, a published event or a cancellation is landed — a REPLY or a COUNTER speaks to an organiser, not to a calendar. A copy the user already holds is rewritten or removed only by its own organiser (RFC 5546 section 3.2.2): a message whose ORGANIZER is not the copy's, or whose copy the user organises themselves, is refused — so an invitee who learnt the UID of an event the user organises on their own server cannot rewrite or remove it from a mail. The sender's UID never becomes a path: the object this service creates is named by a UUID of its own, and found afterwards by the UID inside it, as the sweep finds everything. The addresses the answer is written under are the user's own; no attendee line is resolved to anybody. The link handed back is agenda's own page for the event, built from the platform's domain and the event's id, never from the object.

Cost and limits

This runs on the request thread, after the REPLY left when there was one: at most one default-calendar discovery, two reads of the collection's window, one write of the object, and the answer's read and write. They share one budget (LANDING_BUDGET_PROPERTY, 20 seconds by default): each request is sent with the smaller of the client's own timeout and the time left, none is sent once it is spent, and the landing then fails — told to the user as a calendar that could not be updated. What a spent budget leaves on the server is what any failed step already could: an object written whose import did not follow, which the next sweep brings in. A writer another add-on contributes for the account (CalendarObjectWriters) is bounded only if it sends through this add-on's client, or bounds its own calls by CalDavDeadline.remaining(). A message about one occurrence of a series (a lone RECURRENCE-ID) is refused: landing it means splicing an override into a copy and answering an instance, which this path does not do yet. A newer revision of an event the user holds is written over the server's copy when the mail's SEQUENCE is strictly higher than the copy's master's; whether eXo's own copy then follows is the sweep's freshness rule, as for any change made on the server. A user with no connected account, or no calendar bound on it, is not this add-on's to land for.

Whether the calendar holds an invitation already

Asked by the mail reader every time it shows an invitation it could add (EXO-90873), so it answers from the mapping: the copy the sweep or a landing mapped under the UID on one of the user's own bindings. No collection is read to find one: a copy the mail server filed and no sweep has mapped yet is not told of — the next sweep maps it, within its period, and a click on "Add to my calendar" meanwhile finds it by the landing's own read first, so nothing is written twice. That one collection query per invitation opened, on the reader's request, is the cost this declines. The mapped copy itself is read, one GET of the object at its known href: the user's answer and the copy's SEQUENCE live on the server only, where a phone or a webmail changed them — the sweep brings an imported event in accepted and never carries a later PARTSTAT back. The same rules as a landing say what is not told: one of this deployment's own meetings, a copy the mirror wrote, a message about one occurrence, an event gone from agenda or cancelled there, a copy the server does not serve, a copy the message's organiser is not the organiser of, and one the user organises. Nothing is written.

  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    static final int
    The default of LANDING_BUDGET_PROPERTY: twenty seconds, well above the few round trips a landing makes on a server that answers, and below the client's own 30-second request timeout, so a server that stops answering holds the user's click no longer than one of its requests would.
    static final String
    Deployment property: the seconds one landing's CalDAV calls may take altogether.
  • Constructor Summary

    Constructors
    Constructor
    Description
     
  • Method Summary

    Modifier and Type
    Method
    Description
    org.exoplatform.caldav.model.HeldMailInvitation
    held(String username, String attendeeAddress, String uid, String recurrenceId, String organizer)
    The copy of an invitation the user's calendar holds, for the mail reader to say so when it shows the invitation: the mapped copy, read once from the server, with the user's answer on it and its SEQUENCE.
    boolean
    Whether this add-on holds a calendar for the user: a connected account with a calendar bound on it.
    org.exoplatform.caldav.model.LandedMailInvitation
    land(org.exoplatform.caldav.model.MailInvitation invitation)
    Lands the invitation in the user's calendar: creates or updates the event, answers it, or removes it, as the message and the click say.

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Field Details

    • LANDING_BUDGET_PROPERTY

      public static final String LANDING_BUDGET_PROPERTY
      Deployment property: the seconds one landing's CalDAV calls may take altogether.
      See Also:
    • DEFAULT_LANDING_BUDGET

      public static final int DEFAULT_LANDING_BUDGET
      The default of LANDING_BUDGET_PROPERTY: twenty seconds, well above the few round trips a landing makes on a server that answers, and below the client's own 30-second request timeout, so a server that stops answering holds the user's click no longer than one of its requests would.
      See Also:
  • Constructor Details

    • CaldavInvitationLandingService

      public CaldavInvitationLandingService()
  • Method Details

    • holdsCalendarFor

      public boolean holdsCalendarFor(String username)
      Whether this add-on holds a calendar for the user: a connected account with a calendar bound on it. Database reads only — this is asked on every read of an invitation.
      Parameters:
      username - the user's eXo login
      Returns:
      true when a landing has somewhere to go
    • land

      public org.exoplatform.caldav.model.LandedMailInvitation land(org.exoplatform.caldav.model.MailInvitation invitation)
      Lands the invitation in the user's calendar: creates or updates the event, answers it, or removes it, as the message and the click say.
      Parameters:
      invitation - the invitation, the user and what they asked
      Returns:
      what was done, or null when the user has no connected account or no calendar bound on it, or when there was nothing to do — a decline, or a cancellation, of an event the user never added
      Throws:
      IllegalArgumentException - when the message cannot be landed as it is — unreadable, about another event than the one shown, about one occurrence only, naming no organiser where one is needed, naming a meeting this deployment wrote, or touching a copy of another organiser's
      IllegalStateException - when the landing was attempted and failed, its budget spent included
    • held

      public org.exoplatform.caldav.model.HeldMailInvitation held(String username, String attendeeAddress, String uid, String recurrenceId, String organizer)
      The copy of an invitation the user's calendar holds, for the mail reader to say so when it shows the invitation: the mapped copy, read once from the server, with the user's answer on it and its SEQUENCE. Reads only. What it answers never licenses a change: land(org.exoplatform.caldav.model.MailInvitation) changes or removes a copy the user holds only for a mail the reader established as its organiser's (MailInvitation.fromOrganizer()), whatever this answered.
      Parameters:
      username - the user's eXo login, the reader's authenticated user
      attendeeAddress - the user's mailbox address, possibly null
      uid - the invitation's UID
      recurrenceId - the occurrence the message is about, null for a series or a single event
      organizer - the message's organiser, null for a published event naming none
      Returns:
      the copy held, or null when the user has no calendar here, the UID is not mapped on their bindings, or the copy is not one to tell of
      Throws:
      IllegalStateException - when the mapped copy could not be read