/////////////////////////////////////////////////////////////////////////////
//
// Project ProjectForge Community Edition
// www.projectforge.org
//
// Copyright (C) 2001-2014 Kai Reinhard (k.reinhard@micromata.de)
//
// ProjectForge is dual-licensed.
//
// This community edition is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as published
// by the Free Software Foundation; version 3 of the License.
//
// This community edition is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
// Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see http://www.gnu.org/licenses/.
//
/////////////////////////////////////////////////////////////////////////////
package org.projectforge.plugins.teamcal.event;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.ParameterList;
import net.fortuna.ical4j.model.TimeZoneRegistry;
import net.fortuna.ical4j.model.TimeZoneRegistryFactory;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.component.VTimeZone;
import net.fortuna.ical4j.model.parameter.Cn;
import net.fortuna.ical4j.model.parameter.PartStat;
import net.fortuna.ical4j.model.parameter.Role;
import net.fortuna.ical4j.model.parameter.Rsvp;
import net.fortuna.ical4j.model.property.Attendee;
import net.fortuna.ical4j.model.property.CalScale;
import net.fortuna.ical4j.model.property.Comment;
import net.fortuna.ical4j.model.property.Contact;
import net.fortuna.ical4j.model.property.Location;
import net.fortuna.ical4j.model.property.Method;
import net.fortuna.ical4j.model.property.Organizer;
import net.fortuna.ical4j.model.property.ProdId;
import net.fortuna.ical4j.model.property.RRule;
import net.fortuna.ical4j.model.property.Sequence;
import net.fortuna.ical4j.model.property.Version;
import org.apache.commons.lang.StringUtils;
import org.projectforge.calendar.ICal4JUtils;
import org.projectforge.core.ConfigXml;
import org.projectforge.mail.Mail;
import org.projectforge.mail.SendMail;
import org.projectforge.registry.Registry;
import org.projectforge.scripting.I18n;
import org.projectforge.user.PFUserContext;
import org.projectforge.user.PFUserDO;
import de.micromata.hibernate.history.HistoryEntry;
import de.micromata.hibernate.history.delta.PropertyDelta;
/**
* @author Kai Reinhard (k.reinhard@micromata.de)
*
*/
public class TeamEventMailer
{
private static TeamEventMailer instance = null;
private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(TeamEventMailer.class);
private final Queue<TeamEventMailValue> queue;
private final TeamEventDao teamEventDao;
class Marker {
protected final Date lastEmail;
protected final HistoryEntry[] entries;
private final List<HistoryEntry> list;
private boolean locationChanged;
private boolean dateChanged;
private boolean recurrenceChanged;
public Marker(final HistoryEntry[] entries, final Date lastEmail) {
this.entries = entries;
this.lastEmail=lastEmail;
list = new LinkedList<HistoryEntry>();
for (int i=0; i < entries.length-1; i++) {
if (entries[i].getTimestamp().getTime() > lastEmail.getTime()) {
list.add(entries[i]);
}
}
locationChanged = hasLocationChanged();
dateChanged = hasDateChanged();
recurrenceChanged = hasRecurrenceChanged();
}
private boolean hasLocationChanged() {
for (final HistoryEntry entry : list) {
for (final PropertyDelta delta : entry.getDelta()) {
if (StringUtils.contains(delta.getPropertyName(), "location") == true) {
return true;
}
}
}
return false;
}
private boolean hasDateChanged() {
for (final HistoryEntry entry : list) {
for (final PropertyDelta delta : entry.getDelta()) {
if (StringUtils.contains(delta.getPropertyName(), "startDate") == true ||
StringUtils.contains(delta.getPropertyName(), "endDate") == true ) {
return true;
}
}
}
return false;
}
private boolean hasRecurrenceChanged() {
for (final HistoryEntry entry : list) {
for (final PropertyDelta delta : entry.getDelta()) {
if (StringUtils.contains(delta.getPropertyName(), "recurrenceRule") == true) {
return true;
}
}
}
return false;
}
public void computeChanges(final TeamEventDO event, final TeamEventDO orgEvent) {
if (StringUtils.equals(event.getLocation(), orgEvent.getLocation()) == false) {
locationChanged = true;
}
if (event.getStartDate() != null && orgEvent.getStartDate() != null &&
event.getEndDate() != null && orgEvent.getEndDate() != null) {
if (event.getStartDate().equals(orgEvent.getStartDate()) == false ||
event.getEndDate().equals(orgEvent.getEndDate()) == false) {
dateChanged = true;
}
}
if (StringUtils.equals(event.getRecurrenceRule(), orgEvent.getRecurrenceRule()) == false) {
recurrenceChanged = true;
}
}
public boolean isLocationChanged()
{
return locationChanged;
}
public boolean isDateChanged()
{
return dateChanged;
}
public boolean isRecurrenceChanged()
{
return recurrenceChanged;
}
}
private TeamEventMailer() {
queue = new LinkedList<TeamEventMailValue>();
teamEventDao = Registry.instance().getDao(TeamEventDao.class);
}
public static TeamEventMailer getInstance() {
if (instance == null) {
instance = new TeamEventMailer();
}
return instance;
}
public Queue<TeamEventMailValue> getQueue()
{
return queue;
}
public boolean send() {
int failures = 0;
while (queue.isEmpty() == false) {
final TeamEventMailValue value = queue.poll();
final TeamEventDO event = teamEventDao.getById(value.getId());
if (sendIcsFile(event, value.getType(), value.getOrgId()) == false) {
failures++;
log.error("Can't send all emails for TeamEvent: " + event.getSubject());
} else {
final Timestamp t = new Timestamp(event.getLastUpdate().getTime());
event.setLastEmail(t);
teamEventDao.saveOrUpdate(event);
}
}
return failures == 0 ? true : false;
}
private boolean sendIcsFile(final TeamEventDO event, final TeamEventMailType type, final Integer orgId) {
int failures = 0;
final Marker marker = new Marker(teamEventDao.getHistoryEntries(event), event.getLastEmail());
if (orgId != null) {
final TeamEventDO orgEvent = teamEventDao.getById(orgId);
marker.computeChanges(event, orgEvent);
}
final String content = getICal(event, type);
final Mail msg = new Mail();
msg.setProjectForgeSubject(composeSubject(event, type));
msg.setContentType(Mail.CONTENTTYPE_HTML);
final SendMail sendMail = new SendMail();
sendMail.setConfigXml(ConfigXml.getInstance());
for (final TeamEventAttendeeDO attendee : event.getAttendees()) {
msg.setContent(composeHtmlContent(event, attendee.getNumber(), type, marker));
if (attendee.getUserId() == null) {
msg.setTo(attendee.getUrl());
} else {
msg.setTo(attendee.getUser());
if (attendee.getUser().equals(PFUserContext.getUser()) == true) {
continue;
}
}
switch (type) {
case INVITATION:
if (sendMail.send(msg, content, event.getAttachments()) == false) {
failures++;
}
break;
case UPDATE:
case REJECTION:
if (sendMail.send(msg, content, null) == false) {
failures++;
}
break;
}
}
return failures == 0 ? true : false;
}
private String composeHtmlContent(final TeamEventDO event, final Short number, final TeamEventMailType type, final Marker marker) {
final StringBuffer buf = new StringBuffer();
buf.append("<html><body><h2>").append(composeSubject(event, type)).append("</h2>");
buf.append("<table>");
buf.append(composeDate(event, type, marker));
buf.append(composeRecurrence(event, number, type, marker));
buf.append(composeLocation(event, type, marker));
buf.append(composeAttendees(event, number, type));
buf.append(composeModifier(event, type));
buf.append("</table>");
buf.append(composeButtons(event, number, type));
buf.append("</body></html>");
return buf.toString();
}
private String composeSubject(final TeamEventDO event, final TeamEventMailType type) {
final StringBuffer buf = new StringBuffer();
switch (type) {
case INVITATION: {
buf.append(PFUserContext.getUser().getFullname());
buf.append(" ").append(I18n.getString("plugins.teamcal.event.invitation1"));
buf.append(" „").append(event.getSubject()).append("“ ");
buf.append(I18n.getString("plugins.teamcal.event.invitation2"));
return buf.toString();
}
case UPDATE: {
buf.append(" „").append(event.getSubject()).append("“ ");
buf.append(I18n.getString("plugins.teamcal.event.update"));
return buf.toString();
}
case REJECTION: {
buf.append(" „").append(event.getSubject()).append("“ ");
buf.append(I18n.getString("plugins.teamcal.event.rejection"));
return buf.toString();
}
default:
buf.append("Unsupported TeamEventMailType: ").append(type);
log.error(buf.toString());
return buf.toString();
}
}
private String composeDate(final TeamEventDO event, final TeamEventMailType type, final Marker marker) {
final StringBuffer buf = new StringBuffer();
buf.append("<tr>");
switch (type) {
case UPDATE:
if (marker.isDateChanged() == true) {
buf.append("<td>").append(I18n.getString("plugins.teamcal.event.changed")).append("</td>");
break;
}
case REJECTION:
case INVITATION:
buf.append("<td>").append(I18n.getString("plugins.teamcal.event.event")).append("</td>");
}
final Date d1 = new Date(event.getStartDate().getTime());
final Date d2 = new Date(event.getEndDate().getTime());
final SimpleDateFormat df1 = new SimpleDateFormat("EEEEE, dd. MMMM yyyy, HH:mm");
final SimpleDateFormat df2 = new SimpleDateFormat("HH:mm");
df1.setTimeZone(PFUserContext.getTimeZone());
df2.setTimeZone(PFUserContext.getTimeZone());
final String s = df1.format(d1) + " Uhr - " + df2.format(d2) + " Uhr";
buf.append("<td>").append(s).append("</td>");
buf.append("</tr>");
return buf.toString();
}
private String composeRecurrence(final TeamEventDO event, final Short number, final TeamEventMailType type, final Marker marker) {
final StringBuffer buf = new StringBuffer();
if (StringUtils.isNotBlank(event.getRecurrenceRule()) == true) {
buf.append("<tr>");
switch (type) {
case UPDATE:
if (marker.isRecurrenceChanged() == true) {
buf.append("<td>").append(I18n.getString("plugins.teamcal.event.recurrence.changed")).append("</td>");
} else {
buf.append("<td></td>");
}
buf.append("<td>").append("<a href=\"http://localhost:8080/ProjectForge/Calendar/Event/?e=").append(event.getUid()).append("&p=p").append(number.toString()).append("\">").append(I18n.getString("plugins.teamcal.event.recurrence.show")).append("</a></td>");
break;
case INVITATION:
buf.append("<td></td>");
buf.append("<td>").append("<a href=\"http://localhost:8080/ProjectForge/Calendar/Event/?e=").append(event.getUid()).append("&p=p").append(number.toString()).append("\">").append(I18n.getString("plugins.teamcal.event.recurrence.show")).append("</a></td>");
break;
case REJECTION:
buf.append("<td></td>");
buf.append("<td>").append(I18n.getString("plugins.teamcal.event.recurrence.show")).append("</td>");
break;
}
buf.append("</tr>");
}
return buf.toString();
}
private String composeLocation(final TeamEventDO event, final TeamEventMailType type, final Marker marker) {
final StringBuffer buf = new StringBuffer();
if (StringUtils.isNotBlank(event.getLocation()) == true) {
buf.append("<tr>");
switch (type) {
case INVITATION:
case REJECTION:
buf.append("<td>").append(I18n.getString("plugins.teamcal.event.location")).append("</td>");
break;
case UPDATE:
if (marker.isLocationChanged() ==true) {
buf.append("<td>").append(I18n.getString("plugins.teamcal.event.location.changed")).append("</td>");
} else {
buf.append("<td>").append(I18n.getString("plugins.teamcal.event.location")).append("</td>");
}
}
buf.append("<td>").append(event.getLocation()).append("</td></tr>");
}
return buf.toString();
}
private String composeAttendees(final TeamEventDO event, final Short number, final TeamEventMailType type) {
final StringBuffer buf = new StringBuffer();
if (event.getAttendees() != null || event.getAttendees().isEmpty() == false) {
buf.append("<tr>");
buf.append("<td>").append(I18n.getString("plugins.teamcal.attendees")).append("</td>");
buf.append("<td>").append(PFUserContext.getUser().getFullname()).append(" ").append(I18n.getString("plugins.teamcal.event.andyou")).append("</td>");
buf.append("</tr>");
switch (type) {
case REJECTION:
break;
case INVITATION:
case UPDATE:
buf.append("<tr>");
buf.append("<td></td>");
buf.append("<td>").append("<a href=\"http://localhost:8080/ProjectForge/Calendar/Event/?e=").append(event.getUid()).append("&p=p").append(number.toString()).append("\">").append(I18n.getString("plugins.teamcal.event.showreplies")).append("</a></td>");
buf.append("</tr>");
}
}
return buf.toString();
}
private String composeModifier(final TeamEventDO event, final TeamEventMailType type) {
final StringBuffer buf = new StringBuffer();
switch (type) {
case INVITATION:
break;
case UPDATE:
buf.append("<tr>");
buf.append("<td>").append(I18n.getString("plugins.teamcal.event.changedby")).append("</td>");
buf.append("<td>").append(PFUserContext.getUser().getFullname()).append("</td>");
buf.append("</tr>");
break;
case REJECTION:
buf.append("<tr>");
buf.append("<td>").append(I18n.getString("plugins.teamcal.event.deletedby")).append("</td>");
buf.append("<td>").append(PFUserContext.getUser().getFullname()).append("</td>");
buf.append("</tr>");
}
return buf.toString();
}
private String composeButtons(final TeamEventDO event, final Short number, final TeamEventMailType type) {
final StringBuffer buf = new StringBuffer();
buf.append("</br>");
switch(type) {
case REJECTION:
break;
case INVITATION:
case UPDATE:
buf.append("<table>");
buf.append("<tr>");
buf.append("<td>").append("<a href=\"http://localhost:8080/ProjectForge/Calendar/Eventreply/?e=").append(event.getUid()).append("&p=p").append(number.toString()).append("&r=accept\">").append(I18n.getString("plugins.teamcal.event.accept")).append("</a></td>");
buf.append("<td>").append("<a href=\"http://localhost:8080/ProjectForge/Calendar/Eventreply/?e=").append(event.getUid()).append("&p=p").append(number.toString()).append("&r=decline\">").append(I18n.getString("plugins.teamcal.event.decline")).append("</a></td>");
buf.append("<td>").append("<a href=\"http://localhost:8080/ProjectForge/Calendar/Eventreply/?e=").append(event.getUid()).append("&p=p").append(number.toString()).append("&r=tentative\">").append(I18n.getString("plugins.teamcal.event.tentative")).append("</a></td>");
buf.append("</tr>");
buf.append("</table></br>");
}
return buf.toString();
}
public static String getICal(final TeamEventDO teamEvent, final TeamEventMailType type) {
final StringBuffer buf = new StringBuffer();
final Calendar calendar = new Calendar();
calendar.getProperties().add(new ProdId("-//Ben Fortuna//iCal4j 1.0//EN"));
calendar.getProperties().add(Version.VERSION_2_0);
calendar.getProperties().add(CalScale.GREGORIAN);
final TimeZoneRegistry registry = TimeZoneRegistryFactory.getInstance().createRegistry();
final VTimeZone tz = registry.getTimeZone(PFUserContext.getTimeZone().getID()).getVTimeZone();
calendar.getComponents().add(tz);
switch (type) {
case INVITATION:
case UPDATE:
calendar.getProperties().add(Method.REQUEST);
break;
case REJECTION:
calendar.getProperties().add(Method.CANCEL);
}
final VEvent vEvent = ICal4JUtils.createVEvent(teamEvent.getStartDate(), teamEvent.getEndDate(), teamEvent.getUid(), teamEvent.getSubject(), teamEvent.isAllDay());
vEvent.getProperties().add(new Sequence(teamEvent.getSequence()));
if (teamEvent.hasRecurrence() == true) {
vEvent.getProperties().add(new RRule(teamEvent.getRecurrenceObject()));
}
if (StringUtils.isNotBlank(teamEvent.getLocation()) == true) {
vEvent.getProperties().add(new Location(teamEvent.getLocation()));
}
if (StringUtils.isNotBlank(teamEvent.getNote()) == true) {
vEvent.getProperties().add(new Comment(teamEvent.getNote()));
}
final PFUserDO user = PFUserContext.getUser();
String s = user.getFullname();
if (user.getOrganization() != null) {
s += "\n" + user.getOrganization();
}
if (user.getPersonalPhoneIdentifiers() != null) {
s += "\n" + user.getPersonalPhoneIdentifiers();
}
vEvent.getProperties().add(new Contact(s));
try {
if (StringUtils.isNotBlank(user.getEmail()) == true) {
final ParameterList organizerParams = new ParameterList();
organizerParams.add(new Cn(user.getFullname()));
final Organizer organizer = new Organizer(organizerParams, "mailto:" + user.getEmail());
vEvent.getProperties().add(organizer);
}
} catch (final Exception e) {
log.error("Cant't build organizer " + e.getMessage());
}
if (teamEvent.getAttendees() != null) {
for (final TeamEventAttendeeDO attendee : teamEvent.getAttendees() ) {
final ParameterList attendeeParams = new ParameterList();
if (attendee.getUser() != null) {
try {
attendeeParams.add(new Cn(attendee.getUser().getFullname()));
// attendeeParams.add(new SentBy(attendee.getUser().getEmail()));
attendeeParams.add(new PartStat(attendee.getStatus().name()));
if (attendee.getStatus().equals(TeamAttendeeStatus.NEEDS_ACTION) == true) {
attendeeParams.add(Role.REQ_PARTICIPANT);
attendeeParams.add(Rsvp.TRUE);
}
vEvent.getProperties().add(new Attendee(attendeeParams, "mailto:" + attendee.getUser().getEmail()));
} catch (final Exception e) {
log.error("Cant't build attendee " + e.getMessage());
}
} else {
try {
// attendeeParams.add(new SentBy(attendee.getUrl()));
attendeeParams.add(new PartStat(attendee.getStatus().name()));
if (attendee.getStatus().equals(TeamAttendeeStatus.NEEDS_ACTION) == true) {
attendeeParams.add(Role.REQ_PARTICIPANT);
attendeeParams.add(Rsvp.TRUE);
}
vEvent.getProperties().add(new Attendee(attendeeParams, "mailto:" + attendee.getUrl()));
} catch (final Exception e) {
log.error("Cant't build attendee " + e.getMessage());
}
}
}
}
calendar.getComponents().add(vEvent);
buf.append(calendar.toString());
return buf.toString();
}
}