Changeset View
Standalone View
wallace/module_invitationpolicy.py
Show First 20 Lines • Show All 455 Lines • ▼ Show 20 Lines | elif policy & ACT_UPDATE and existing: | ||||
rsvp = False | rsvp = False | ||||
# retain task status and percent-complete properties from my old copy | # retain task status and percent-complete properties from my old copy | ||||
if is_task: | if is_task: | ||||
itip_event['xml'].set_status(existing.get_status()) | itip_event['xml'].set_status(existing.get_status()) | ||||
itip_event['xml'].set_percentcomplete(existing.get_percentcomplete()) | itip_event['xml'].set_percentcomplete(existing.get_percentcomplete()) | ||||
if policy & COND_NOTIFY: | if policy & COND_NOTIFY: | ||||
send_update_notification(itip_event['xml'], receiving_user, existing, False) | sender = itip_event['xml'].get_organizer() | ||||
comment = itip_event['xml'].get_comment() | |||||
send_update_notification(itip_event['xml'], receiving_user, existing, False, | |||||
sender, comment) | |||||
vanmeeuwen: How is the sender the organizer, and not the user responding? | |||||
Not Done Inline ActionsBecause only organizer sends REQUEST messages. And we use this only to add "<Sender> commented..." in the notification. So, I think it's reasonably safe assumption. Getting sender from mail headers would be more complicated. machniak: Because only organizer sends REQUEST messages. And we use this only to add "<Sender> commented.. | |||||
Not Done Inline ActionsWhy would we bounce the sender and comment about like this, noted that send_update_notification is already supplied with the itip_event['xml'] that contains both the organizer and the comment? vanmeeuwen: Why would we bounce the sender and comment about like this, noted that send_update_notification… | |||||
Not Done Inline ActionsIt's because there's a case when we don't use itip_event['xml'] as an argument for send_update_notification(). machniak: It's because there's a case when we don't use itip_event['xml'] as an argument for… | |||||
# if RSVP, send an iTip REPLY | # if RSVP, send an iTip REPLY | ||||
if rsvp or scheduling_required: | if rsvp or scheduling_required: | ||||
# set attendee's CN from LDAP record if yet missing | # set attendee's CN from LDAP record if yet missing | ||||
if not receiving_attendee.get_name() and receiving_user.has_key('cn'): | if not receiving_attendee.get_name() and receiving_user.has_key('cn'): | ||||
receiving_attendee.set_name(receiving_user['cn']) | receiving_attendee.set_name(receiving_user['cn']) | ||||
# send iTip reply | # send iTip reply | ||||
▲ Show 20 Lines • Show All 106 Lines • ▼ Show 20 Lines | if policy & ACT_UPDATE: | ||||
log.debug(_("Update delegator: %r") % (existing_attendee.to_dict()), level=9) | log.debug(_("Update delegator: %r") % (existing_attendee.to_dict()), level=9) | ||||
except Exception, errmsg: | except Exception, errmsg: | ||||
log.error("Could not find delegated-to attendee: %r" % (errmsg)) | log.error("Could not find delegated-to attendee: %r" % (errmsg)) | ||||
# update the organizer's copy of the object | # update the organizer's copy of the object | ||||
if update_object(existing, receiving_user, master): | if update_object(existing, receiving_user, master): | ||||
if policy & COND_NOTIFY: | if policy & COND_NOTIFY: | ||||
send_update_notification(existing, receiving_user, existing, True) | send_update_notification(existing, receiving_user, existing, True, | ||||
sender_attendee, itip_event['xml'].get_comment()) | |||||
Not Done Inline ActionsHere, the sender is a sender_attendee? vanmeeuwen: Here, the sender is a sender_attendee? | |||||
Not Done Inline ActionsYes, because this is iTip REPLY. machniak: Yes, because this is iTip REPLY. | |||||
# update all other attendee's copies | # update all other attendee's copies | ||||
if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'): | if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'): | ||||
propagate_changes_to_attendees_accounts(existing, updated_attendees) | propagate_changes_to_attendees_accounts(existing, updated_attendees) | ||||
return MESSAGE_PROCESSED | return MESSAGE_PROCESSED | ||||
else: | else: | ||||
▲ Show 20 Lines • Show All 47 Lines • ▼ Show 20 Lines | if policy & ACT_UPDATE or policy & ACT_CANCEL_DELETE: | ||||
log.debug(_("Update cancelled %s %r with STATUS=CANCELLED") % (existing.type, existing.uid), level=8) | log.debug(_("Update cancelled %s %r with STATUS=CANCELLED") % (existing.type, existing.uid), level=8) | ||||
existing.set_status('CANCELLED') | existing.set_status('CANCELLED') | ||||
existing.set_transparency(True) | existing.set_transparency(True) | ||||
success = update_object(existing, receiving_user, master) | success = update_object(existing, receiving_user, master) | ||||
if success: | if success: | ||||
# send cancellation notification | # send cancellation notification | ||||
if policy & COND_NOTIFY: | if policy & COND_NOTIFY: | ||||
send_cancel_notification(existing, receiving_user, remove_object) | sender = itip_event['xml'].get_organizer() | ||||
comment = itip_event['xml'].get_comment() | |||||
send_cancel_notification(existing, receiving_user, remove_object, sender, comment) | |||||
Not Done Inline ActionsSame comments/questions as before on 464-467. vanmeeuwen: Same comments/questions as before on 464-467. | |||||
Not Done Inline ActionsSame answers as above. This is iTip CANCEL and we use this only to add "<Sender> commented..." to the notification message. machniak: Same answers as above. This is iTip CANCEL and we use this only to add "<Sender> commented..."… | |||||
return MESSAGE_PROCESSED | return MESSAGE_PROCESSED | ||||
else: | else: | ||||
log.error(_("The object referred by this cancel request was not found in the user's folders. Forwarding to Inbox.")) | log.error(_("The object referred by this cancel request was not found in the user's folders. Forwarding to Inbox.")) | ||||
return MESSAGE_FORWARD | return MESSAGE_FORWARD | ||||
return None | return None | ||||
▲ Show 20 Lines • Show All 107 Lines • ▼ Show 20 Lines | |||||
def list_user_folders(user_rec, type): | def list_user_folders(user_rec, type): | ||||
""" | """ | ||||
Get a list of the given user's private calendar/tasks folders | Get a list of the given user's private calendar/tasks folders | ||||
""" | """ | ||||
global imap | global imap | ||||
# return cached list | # return cached list | ||||
if user_rec.has_key('_imap_folders'): | if user_rec.has_key('_imap_folders'): | ||||
return user_rec['_imap_folders']; | return user_rec['_imap_folders'] | ||||
result = [] | result = [] | ||||
if not imap_proxy_auth(user_rec): | if not imap_proxy_auth(user_rec): | ||||
return result | return result | ||||
folders = imap.list_folders('*') | folders = imap.list_folders('*') | ||||
log.debug(_("List %r folders for user %r: %r") % (type, user_rec['mail'], folders), level=8) | log.debug(_("List %r folders for user %r: %r") % (type, user_rec['mail'], folders), level=8) | ||||
(ns_personal, ns_other, ns_shared) = imap.namespaces() | (ns_personal, ns_other, ns_shared) = imap.namespaces() | ||||
for folder in folders: | for folder in folders: | ||||
# exclude shared and other user's namespace | # exclude shared and other user's namespace | ||||
if not ns_other is None and folder.startswith(ns_other) and user_rec.has_key('_delegated_mailboxes'): | if ns_other is not None and folder.startswith(ns_other) and '_delegated_mailboxes' in user_rec: | ||||
# allow shared folders from delegators | # allow shared folders from delegators | ||||
if len([_mailbox for _mailbox in user_rec['_delegated_mailboxes'] if folder.startswith(ns_other + _mailbox + '/')]) == 0: | if len([_mailbox for _mailbox in user_rec['_delegated_mailboxes'] if folder.startswith(ns_other + _mailbox + '/')]) == 0: | ||||
continue; | continue | ||||
# TODO: list shared folders the user has write privileges ? | # TODO: list shared folders the user has write privileges ? | ||||
if not ns_shared is None and len([_ns for _ns in ns_shared if folder.startswith(_ns)]) > 0: | if ns_shared is not None and len([_ns for _ns in ns_shared if folder.startswith(_ns)]) > 0: | ||||
continue; | continue | ||||
metadata = imap.get_metadata(folder) | metadata = imap.get_metadata(folder) | ||||
log.debug(_("IMAP metadata for %r: %r") % (folder, metadata), level=9) | log.debug(_("IMAP metadata for %r: %r") % (folder, metadata), level=9) | ||||
if metadata.has_key(folder) and ( \ | if metadata.has_key(folder) and ( \ | ||||
metadata[folder].has_key('/shared' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/shared' + FOLDER_TYPE_ANNOTATION].startswith(type) \ | metadata[folder].has_key('/shared' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/shared' + FOLDER_TYPE_ANNOTATION].startswith(type) \ | ||||
or metadata[folder].has_key('/private' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/private' + FOLDER_TYPE_ANNOTATION].startswith(type)): | or metadata[folder].has_key('/private' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/private' + FOLDER_TYPE_ANNOTATION].startswith(type)): | ||||
result.append(folder) | result.append(folder) | ||||
▲ Show 20 Lines • Show All 291 Lines • ▼ Show 20 Lines | def delete_object(existing): | ||||
except Exception, errmsg: | except Exception, errmsg: | ||||
log.error(_("Failed to delete %s from folder %r: %r") % ( | log.error(_("Failed to delete %s from folder %r: %r") % ( | ||||
existing.type, targetfolder, errmsg | existing.type, targetfolder, errmsg | ||||
)) | )) | ||||
return False | return False | ||||
def send_update_notification(object, receiving_user, old=None, reply=True): | def send_update_notification(object, receiving_user, old=None, reply=True, sender=None, comment=None): | ||||
""" | """ | ||||
Send a (consolidated) notification about the current participant status to organizer | Send a (consolidated) notification about the current participant status to organizer | ||||
""" | """ | ||||
global auth | global auth | ||||
import smtplib | import smtplib | ||||
from email.MIMEText import MIMEText | from email.MIMEText import MIMEText | ||||
from email.Utils import formatdate | from email.Utils import formatdate | ||||
from email.header import Header | from email.header import Header | ||||
from email import charset | from email import charset | ||||
# encode unicode strings with quoted-printable | # encode unicode strings with quoted-printable | ||||
charset.add_charset('utf-8', charset.SHORTEST, charset.QP) | charset.add_charset('utf-8', charset.SHORTEST, charset.QP) | ||||
organizer = object.get_organizer() | organizer = object.get_organizer() | ||||
orgemail = organizer.email() | orgemail = organizer.email() | ||||
orgname = organizer.name() | orgname = organizer.name() | ||||
itip_comment = None | |||||
if sender is not None and not comment == '': | |||||
itip_comment = _("%s commented: %s") % (_attendee_name(sender), comment) | |||||
Not Done Inline ActionsWhat is the 'sender' here is the initiator of a request or reply -- for requests, 'old is None', and for replies, I reckon 'old is not None', for a reply is supposed to be a mutation of an orignal. vanmeeuwen: What is the 'sender' here is the initiator of a request or reply -- for requests, 'old is None'… | |||||
Not Done Inline ActionsThis is just a sanity check. We add comment to the notification if we know the sender and there's a comment. machniak: This is just a sanity check. We add comment to the notification if we know the sender and… | |||||
if reply: | if reply: | ||||
log.debug(_("Compose participation status summary for %s %r to user %r") % ( | log.debug(_("Compose participation status summary for %s %r to user %r") % ( | ||||
object.type, object.uid, receiving_user['mail'] | object.type, object.uid, receiving_user['mail'] | ||||
), level=8) | ), level=8) | ||||
auto_replies_expected = 0 | auto_replies_expected = 0 | ||||
auto_replies_received = 0 | auto_replies_received = 0 | ||||
is_manual_reply = True | |||||
partstats = { 'ACCEPTED':[], 'TENTATIVE':[], 'DECLINED':[], 'DELEGATED':[], 'IN-PROCESS':[], 'COMPLETED':[], 'PENDING':[] } | partstats = {'ACCEPTED': [], 'TENTATIVE': [], 'DECLINED': [], 'DELEGATED': [], 'IN-PROCESS': [], 'COMPLETED': [], 'PENDING': []} | ||||
Not Done Inline ActionsI thought we had agreed to determine whether the attendee (whom this reply concerns) is an attendee for which the policy would dictate an automated reply be issued using is_auto_reply()? vanmeeuwen: I thought we had agreed to determine whether the attendee (whom this reply concerns) is an… | |||||
for attendee in object.get_attendees(): | for attendee in object.get_attendees(): | ||||
parstat = attendee.get_participant_status(True) | parstat = attendee.get_participant_status(True) | ||||
if partstats.has_key(parstat): | if partstats.has_key(parstat): | ||||
partstats[parstat].append(attendee.get_displayname()) | partstats[parstat].append(attendee.get_displayname()) | ||||
else: | else: | ||||
partstats['PENDING'].append(attendee.get_displayname()) | partstats['PENDING'].append(attendee.get_displayname()) | ||||
# look-up kolabinvitationpolicy for this attendee | # look-up kolabinvitationpolicy for this attendee | ||||
if attendee.get_cutype() == kolabformat.CutypeResource: | if attendee.get_cutype() == kolabformat.CutypeResource: | ||||
resource_dns = auth.find_resource(attendee.get_email()) | resource_dns = auth.find_resource(attendee.get_email()) | ||||
if isinstance(resource_dns, list): | if isinstance(resource_dns, list): | ||||
attendee_dn = resource_dns[0] if len(resource_dns) > 0 else None | attendee_dn = resource_dns[0] if len(resource_dns) > 0 else None | ||||
else: | else: | ||||
attendee_dn = resource_dns | attendee_dn = resource_dns | ||||
else: | else: | ||||
attendee_dn = user_dn_from_email_address(attendee.get_email()) | attendee_dn = user_dn_from_email_address(attendee.get_email()) | ||||
if attendee_dn: | if attendee_dn: | ||||
attendee_rec = auth.get_entry_attributes(None, attendee_dn, ['kolabinvitationpolicy']) | attendee_rec = auth.get_entry_attributes(None, attendee_dn, ['kolabinvitationpolicy']) | ||||
if is_auto_reply(attendee_rec, orgemail, object.type): | if is_auto_reply(attendee_rec, orgemail, object.type): | ||||
auto_replies_expected += 1 | auto_replies_expected += 1 | ||||
if not parstat == 'NEEDS-ACTION': | if not parstat == 'NEEDS-ACTION': | ||||
auto_replies_received += 1 | auto_replies_received += 1 | ||||
if sender is not None and sender.get_email() == attendee.get_email(): | |||||
is_manual_reply = False | |||||
Not Done Inline ActionsI do not understand how this clause validates. vanmeeuwen: I do not understand how this clause validates. | |||||
Not Done Inline ActionsThis is a loop over all attendees. So, if sender is defined and sender email equals currently processed attendee's email and this is auto-reply we unset the manual reply. I think this way we'll have less false-positives. machniak: This is a loop over all attendees. So, if sender is defined and sender email equals currently… | |||||
# skip notification until we got replies from all automatically responding attendees | # skip notification until we got replies from all automatically responding attendees | ||||
if auto_replies_received < auto_replies_expected: | if not is_manual_reply and auto_replies_received < auto_replies_expected: | ||||
log.debug(_("Waiting for more automated replies (got %d of %d); skipping notification") % ( | log.debug(_("Waiting for more automated replies (got %d of %d); skipping notification") % ( | ||||
auto_replies_received, auto_replies_expected | auto_replies_received, auto_replies_expected | ||||
), level=8) | ), level=8) | ||||
return | return | ||||
# build notification message body | |||||
roundup = '' | roundup = '' | ||||
if itip_comment is not None: | |||||
roundup += "\n" + itip_comment | |||||
for status,attendees in partstats.iteritems(): | for status,attendees in partstats.iteritems(): | ||||
if len(attendees) > 0: | if len(attendees) > 0: | ||||
roundup += "\n" + participant_status_label(status) + ":\n" + "\n".join(attendees) + "\n" | roundup += "\n" + participant_status_label(status) + ":\n\t" + "\n\t".join(attendees) + "\n" | ||||
else: | else: | ||||
roundup = "\n" + _("Changes submitted by %s have been automatically applied.") % (orgname if orgname else orgemail) | # build notification message body | ||||
roundup = '' | |||||
if itip_comment is not None: | |||||
roundup += "\n" + itip_comment | |||||
roundup += "\n" + _("Changes submitted by %s have been automatically applied.") % (orgname if orgname else orgemail) | |||||
# list properties changed from previous version | # list properties changed from previous version | ||||
if old: | if old: | ||||
diff = xmlutils.compute_diff(old.to_dict(), object.to_dict()) | diff = xmlutils.compute_diff(old.to_dict(), object.to_dict()) | ||||
if len(diff) > 1: | if len(diff) > 1: | ||||
roundup += "\n" | roundup += "\n" | ||||
for change in diff: | for change in diff: | ||||
if not change['property'] in ['created','lastmodified-date','sequence']: | if not change['property'] in ['created','lastmodified-date','sequence']: | ||||
▲ Show 20 Lines • Show All 52 Lines • ▼ Show 20 Lines | while not success and retries > 0: | ||||
log.error(_("SMTP sendmail error: %r") % (errmsg)) | log.error(_("SMTP sendmail error: %r") % (errmsg)) | ||||
time.sleep(10) | time.sleep(10) | ||||
retries -= 1 | retries -= 1 | ||||
return success | return success | ||||
def send_cancel_notification(object, receiving_user, deleted=False): | def send_cancel_notification(object, receiving_user, deleted=False, sender=None, comment=None): | ||||
""" | """ | ||||
Send a notification about event/task cancellation | Send a notification about event/task cancellation | ||||
""" | """ | ||||
import smtplib | import smtplib | ||||
from email.MIMEText import MIMEText | from email.MIMEText import MIMEText | ||||
from email.Utils import formatdate | from email.Utils import formatdate | ||||
from email.header import Header | from email.header import Header | ||||
from email import charset | from email import charset | ||||
Show All 25 Lines | else: | ||||
'start': xmlutils.property_to_string('start', object.get_start()), | 'start': xmlutils.property_to_string('start', object.get_start()), | ||||
'organizer': orgname if orgname else orgemail | 'organizer': orgname if orgname else orgemail | ||||
} | } | ||||
if deleted: | if deleted: | ||||
message_text += " " + _("The copy in your calendar has been removed accordingly.") | message_text += " " + _("The copy in your calendar has been removed accordingly.") | ||||
else: | else: | ||||
message_text += " " + _("The copy in your calendar has been marked as cancelled accordingly.") | message_text += " " + _("The copy in your calendar has been marked as cancelled accordingly.") | ||||
if sender is not None and not comment == '': | |||||
message_text += "\n" + _("%s commented: %s") % (_attendee_name(sender), comment) | |||||
if object.get_recurrence_id(): | if object.get_recurrence_id(): | ||||
message_text += "\n" + _("NOTE: This cancellation only refers to this single occurrence!") | message_text += "\n" + _("NOTE: This cancellation only refers to this single occurrence!") | ||||
message_text += "\n\n" + _("*** This is an automated message. Please do not reply. ***") | message_text += "\n\n" + _("*** This is an automated message. Please do not reply. ***") | ||||
# compose mime message | # compose mime message | ||||
msg = MIMEText(utils.stripped_message(message_text), _charset='utf-8') | msg = MIMEText(utils.stripped_message(message_text), _charset='utf-8') | ||||
▲ Show 20 Lines • Show All 64 Lines • ▼ Show 20 Lines | for attendee in object.get_attendees(): | ||||
try: | try: | ||||
attendee_entry = attendee_object.get_attendee_by_email(attendee_email) | attendee_entry = attendee_object.get_attendee_by_email(attendee_email) | ||||
except: | except: | ||||
attendee_entry = None | attendee_entry = None | ||||
if attendee_entry: | if attendee_entry: | ||||
break | break | ||||
# copy all attendees from master object (covers additions and removals) | # copy all attendees from master object (covers additions and removals) | ||||
new_attendees = []; | new_attendees = [] | ||||
for a in object.get_attendees(): | for a in object.get_attendees(): | ||||
# keep my own entry intact | # keep my own entry intact | ||||
if attendee_entry is not None and attendee_entry.get_email() == a.get_email(): | if attendee_entry is not None and attendee_entry.get_email() == a.get_email(): | ||||
new_attendees.append(attendee_entry) | new_attendees.append(attendee_entry) | ||||
else: | else: | ||||
new_attendees.append(a) | new_attendees.append(a) | ||||
attendee_object.set_attendees(new_attendees) | attendee_object.set_attendees(new_attendees) | ||||
Show All 14 Lines | |||||
def invitation_response_text(type): | def invitation_response_text(type): | ||||
footer = "\n\n" + _("*** This is an automated message. Please do not reply. ***") | footer = "\n\n" + _("*** This is an automated message. Please do not reply. ***") | ||||
if type == 'task': | if type == 'task': | ||||
return _("%(name)s has %(status)s your assignment for %(summary)s.") + footer | return _("%(name)s has %(status)s your assignment for %(summary)s.") + footer | ||||
else: | else: | ||||
return _("%(name)s has %(status)s your invitation for %(summary)s.") + footer | return _("%(name)s has %(status)s your invitation for %(summary)s.") + footer | ||||
def _attendee_name(attendee): | |||||
# attendee here can be Attendee or ContactReference | |||||
try: | |||||
name = attendee.get_name() | |||||
except Exception: | |||||
name = attendee.name() | |||||
if name == '': | |||||
try: | |||||
name = attendee.get_email() | |||||
except Exception: | |||||
name = attendee.email() | |||||
return name |
How is the sender the organizer, and not the user responding?