Added calendar replies

This commit is contained in:
M66B 2019-05-18 19:44:21 +02:00
parent 29dc3a9f8d
commit 189a24bc8b
5 changed files with 243 additions and 31 deletions

View File

@ -50,6 +50,7 @@ This app starts a foreground service with a low priority status bar notification
* Send messages after selected time
* Synchronization scheduling ([instructions](https://github.com/M66B/open-source-email/blob/master/FAQ.md#user-content-faq78))
* Reply templates
* Accept/decline calendar invitations
* Filter rules ([instructions](https://github.com/M66B/open-source-email/blob/master/FAQ.md#user-content-faq71))
* Search on device or server ([instructions](https://github.com/M66B/open-source-email/blob/master/FAQ.md#user-content-faq13))
* Keyword management

View File

@ -124,7 +124,10 @@ import javax.mail.internet.InternetAddress;
import biweekly.Biweekly;
import biweekly.ICalendar;
import biweekly.component.VEvent;
import biweekly.parameter.ParticipationStatus;
import biweekly.property.Attendee;
import biweekly.property.Method;
import biweekly.property.Organizer;
import biweekly.property.Summary;
import biweekly.util.ICalDate;
@ -264,6 +267,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
private TextView tvCalendarStart;
private TextView tvCalendarEnd;
private TextView tvAttendees;
private Button btnCalendarAccept;
private Button btnCalendarDecline;
private Button btnCalendarMaybe;
private ContentLoadingProgressBar pbCalendarWait;
private RecyclerView rvImage;
@ -271,6 +277,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
private Group grpAddresses;
private Group grpHeaders;
private Group grpCalendar;
private Group grpCalendarResponse;
private Group grpAttachments;
private Group grpImages;
@ -351,7 +358,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
tvCalendarStart = view.findViewById(R.id.tvCalendarStart);
tvCalendarEnd = view.findViewById(R.id.tvCalendarEnd);
tvAttendees = view.findViewById(R.id.tvAttendees);
btnCalendarAccept = view.findViewById(R.id.btnCalendarAccept);
btnCalendarDecline = view.findViewById(R.id.btnCalendarDecline);
btnCalendarMaybe = view.findViewById(R.id.btnCalendarMaybe);
pbCalendarWait = view.findViewById(R.id.pbCalendarWait);
rvAttachment = attachments.findViewById(R.id.rvAttachment);
@ -389,6 +398,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
grpAddresses = itemView.findViewById(R.id.grpAddresses);
grpHeaders = itemView.findViewById(R.id.grpHeaders);
grpCalendar = itemView.findViewById(R.id.grpCalendar);
grpCalendarResponse = itemView.findViewById(R.id.grpCalendarResponse);
grpAttachments = attachments.findViewById(R.id.grpAttachments);
grpImages = itemView.findViewById(R.id.grpImages);
}
@ -431,6 +441,10 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
ibImages.setOnClickListener(this);
ibFull.setOnClickListener(this);
btnCalendarAccept.setOnClickListener(this);
btnCalendarDecline.setOnClickListener(this);
btnCalendarMaybe.setOnClickListener(this);
bnvActions.setOnNavigationItemSelectedListener(this);
}
@ -440,18 +454,26 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
ivExpander.setOnClickListener(null);
} else
view.setOnClickListener(null);
ivSnoozed.setOnClickListener(null);
ivFlagged.setOnClickListener(null);
ivExpanderAddress.setOnClickListener(null);
ibSearchContact.setOnClickListener(null);
ibNotifyContact.setOnClickListener(null);
ibAddContact.setOnClickListener(null);
btnDownloadAttachments.setOnClickListener(null);
btnSaveAttachments.setOnClickListener(null);
tbHtml.setOnCheckedChangeListener(null);
ibImages.setOnClickListener(null);
ibFull.setOnClickListener(null);
btnCalendarAccept.setOnClickListener(null);
btnCalendarDecline.setOnClickListener(null);
btnCalendarMaybe.setOnClickListener(null);
bnvActions.setOnNavigationItemSelectedListener(null);
}
@ -716,6 +738,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
grpAddresses.setVisibility(View.GONE);
grpHeaders.setVisibility(View.GONE);
grpCalendar.setVisibility(View.GONE);
grpCalendarResponse.setVisibility(View.GONE);
grpAttachments.setVisibility(View.GONE);
grpImages.setVisibility(View.GONE);
@ -1042,11 +1065,12 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
if (show_inline || !inline)
a.add(attachment);
if (attachment.available && "text/calendar".endsWith(attachment.type)) {
if (attachment.available && "text/calendar".equals(attachment.type)) {
// https://tools.ietf.org/html/rfc5546
calendar = true;
Bundle args = new Bundle();
args.putLong("id", message.id);
args.putSerializable("file", attachment.getFile(context));
new SimpleTask<ICalendar>() {
@ -1069,11 +1093,17 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
@Override
protected void onExecuted(Bundle args, ICalendar icalendar) {
long id = args.getLong("id");
TupleMessageEx amessage = getMessage();
if (amessage == null || !amessage.id.equals(id))
return;
if (icalendar == null || icalendar.getEvents().size() == 0)
return;
DateFormat df = SimpleDateFormat.getDateTimeInstance();
Method method = icalendar.getMethod();
VEvent event = icalendar.getEvents().get(0);
Summary summary = event.getSummary();
@ -1096,6 +1126,8 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
}
}
Organizer organizer = event.getOrganizer();
tvCalendarSummary.setText(summary == null ? null : summary.getValue());
tvCalendarSummary.setVisibility(summary == null ? View.GONE : View.VISIBLE);
@ -1107,6 +1139,12 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
tvAttendees.setText(TextUtils.join(", ", attendee));
tvAttendees.setVisibility(attendee.size() == 0 ? View.GONE : View.VISIBLE);
boolean canRespond =
(method != null && method.isRequest() &&
organizer != null && organizer.getEmail() != null &&
message.to != null && message.to.length > 0);
grpCalendarResponse.setVisibility(canRespond ? View.VISIBLE : View.GONE);
}
@Override
@ -1125,6 +1163,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
tvAttendees.setVisibility(View.GONE);
pbCalendarWait.setVisibility(View.GONE);
grpCalendar.setVisibility(View.GONE);
grpCalendarResponse.setVisibility(View.GONE);
}
cbInline.setOnCheckedChangeListener(null);
@ -1162,6 +1201,86 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
showText(message);
}
private void onActionCalendar(TupleMessageEx message, int action) {
Bundle args = new Bundle();
args.putLong("id", message.id);
args.putInt("action", action);
new SimpleTask<File>() {
@Override
protected File onExecute(Context context, Bundle args) throws Throwable {
long id = args.getLong("id");
int action = args.getInt("action");
DB db = DB.getInstance(context);
EntityMessage message = db.message().getMessage(id);
if (message == null)
return null;
List<EntityAttachment> attachments = db.attachment().getAttachments(id);
for (EntityAttachment attachment : attachments)
if (attachment.available && "text/calendar".equals(attachment.type)) {
File file = attachment.getFile(context);
ICalendar icalendar = Biweekly.parse(file).first();
VEvent event = icalendar.getEvents().get(0);
// https://tools.ietf.org/html/rfc5546#section-4.2.2
VEvent ev = new VEvent();
ev.setOrganizer(event.getOrganizer());
ev.setUid(event.getUid());
if (event.getSequence() != null)
ev.setSequence(event.getSequence());
InternetAddress to = (InternetAddress) message.to[0];
Attendee attendee = new Attendee(to.getPersonal(), to.getAddress());
switch (action) {
case R.id.btnCalendarAccept:
attendee.setParticipationStatus(ParticipationStatus.ACCEPTED);
break;
case R.id.btnCalendarDecline:
attendee.setParticipationStatus(ParticipationStatus.DECLINED);
break;
case R.id.btnCalendarMaybe:
attendee.setParticipationStatus(ParticipationStatus.TENTATIVE);
break;
}
ev.addAttendee(attendee);
ICalendar response = new ICalendar();
response.setMethod(Method.REPLY);
response.addEvent(ev);
File dir = new File(context.getFilesDir(), "temporary");
if (!dir.exists())
dir.mkdir();
File ics = new File(dir, "meeting.ics");
response.write(ics);
return ics;
}
return null;
}
@Override
protected void onExecuted(Bundle args, File ics) {
Intent reply = new Intent(context, ActivityCompose.class)
.putExtra("action", "participation")
.putExtra("reference", args.getLong("id"))
.putExtra("ics", ics);
context.startActivity(reply);
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(context, owner, ex);
}
}.execute(context, owner, args, "message:participation");
}
private TupleMessageEx getMessage() {
int pos = getAdapterPosition();
if (pos == RecyclerView.NO_POSITION)
@ -1187,18 +1306,30 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
else if (view.getId() == R.id.ibAddContact)
onAddContact(message);
else if (viewType == ViewType.THREAD) {
if (view.getId() == R.id.ivExpanderAddress)
onToggleAddresses(message);
else if (view.getId() == R.id.btnDownloadAttachments)
onDownloadAttachments(message);
else if (view.getId() == R.id.btnSaveAttachments)
onSaveAttachments(message);
else if (view.getId() == R.id.ibImages)
onShowImages(message);
else if (view.getId() == R.id.ibFull)
onShowFull(message);
else
onToggleMessage(message);
switch (view.getId()) {
case R.id.ivExpanderAddress:
onToggleAddresses(message);
break;
case R.id.btnDownloadAttachments:
onDownloadAttachments(message);
break;
case R.id.btnSaveAttachments:
onSaveAttachments(message);
break;
case R.id.ibImages:
onShowImages(message);
break;
case R.id.ibFull:
onShowFull(message);
break;
case R.id.btnCalendarAccept:
case R.id.btnCalendarDecline:
case R.id.btnCalendarMaybe:
onActionCalendar(message, view.getId());
break;
default:
onToggleMessage(message);
}
} else {
vwRipple.setPressed(true);
vwRipple.setPressed(false);
@ -1697,11 +1828,6 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
showText(message);
}
private class OriginalMessage {
String html;
boolean has_images;
}
private String themeHtml(String html) {
if (dark) {
String color = String.format("#%06X", (textColorSecondary & 0xFFFFFF));
@ -2199,12 +2325,6 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
});
}
private class ActionData {
boolean hasJunk;
boolean delete;
TupleMessageEx message;
}
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
ActionData data = (ActionData) bnvActions.getTag();
@ -3177,6 +3297,17 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
Long getKey() {
return getKeyAtPosition(getAdapterPosition());
}
private class OriginalMessage {
String html;
boolean has_images;
}
private class ActionData {
boolean hasJunk;
boolean delete;
TupleMessageEx message;
}
}
AdapterMessage(Context context, LifecycleOwner owner,

View File

@ -659,6 +659,7 @@ public class FragmentCompose extends FragmentBase {
args.putLong("id", getArguments().getLong("id", -1));
args.putLong("account", getArguments().getLong("account", -1));
args.putLong("reference", getArguments().getLong("reference", -1));
args.putSerializable("ics", getArguments().getSerializable("ics"));
args.putBoolean("raw", getArguments().getBoolean("raw", false));
args.putLong("answer", getArguments().getLong("answer", -1));
args.putString("to", getArguments().getString("to"));
@ -1937,6 +1938,7 @@ public class FragmentCompose extends FragmentBase {
String action = args.getString("action");
long id = args.getLong("id", -1);
long reference = args.getLong("reference", -1);
File ics = (File) args.getSerializable("ics");
long answer = args.getLong("answer", -1);
Log.i("Load draft action=" + action + " id=" + id + " reference=" + reference);
@ -2013,7 +2015,8 @@ public class FragmentCompose extends FragmentBase {
body = EntityAnswer.getAnswerText(db, answer, null) + body;
} else {
if ("reply".equals(action) || "reply_all".equals(action) ||
"list".equals(action) || "receipt".equals(action)) {
"list".equals(action) || "receipt".equals(action) ||
"participation".equals(action)) {
if (ref.to != null && ref.to.length > 0) {
String to = ((InternetAddress) ref.to[0]).getAddress();
int at = to.indexOf('@');
@ -2072,14 +2075,14 @@ public class FragmentCompose extends FragmentBase {
} else if ("receipt".equals(action)) {
draft.receipt_request = true;
}
} else if ("forward".equals(action)) {
draft.thread = draft.msgid; // new thread
draft.from = ref.to;
}
String subject = (ref.subject == null ? "" : ref.subject);
if ("reply".equals(action) || "reply_all".equals(action)) {
if ("reply".equals(action) || "reply_all".equals(action) ||
"participation".equals(action)) {
String re = context.getString(R.string.title_subject_reply, "");
if (!prefix_once || !subject.startsWith(re))
draft.subject = context.getString(R.string.title_subject_reply, subject);
@ -2172,6 +2175,19 @@ public class FragmentCompose extends FragmentBase {
HtmlHelper.getPreview(body),
null);
if ("participation".equals(action)) {
EntityAttachment attachment = new EntityAttachment();
attachment.message = draft.id;
attachment.sequence = 1;
attachment.name = ics.getName();
attachment.type = "text/calendar";
attachment.size = ics.length();
attachment.progress = null;
attachment.available = true;
attachment.id = db.attachment().insertAttachment(attachment);
ics.renameTo(attachment.getFile(context));
}
Core.updateMessageSize(context, draft.id);
// Write reference text

View File

@ -68,6 +68,9 @@ import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeUtility;
import javax.mail.internet.ParseException;
import biweekly.Biweekly;
import biweekly.ICalendar;
public class MessageHelper {
private MimeMessage imessage;
@ -373,22 +376,38 @@ public class MessageHelper {
for (final EntityAttachment attachment : attachments)
if (attachment.available) {
BodyPart bpAttachment = new MimeBodyPart();
bpAttachment.setFileName(attachment.name);
File file = attachment.getFile(context);
FileDataSource dataSource = new FileDataSource(file);
dataSource.setFileTypeMap(new FileTypeMap() {
@Override
public String getContentType(File file) {
// https://tools.ietf.org/html/rfc6047
if ("text/calendar".equals(attachment.type))
try {
ICalendar icalendar = Biweekly.parse(file).first();
if (icalendar != null &&
icalendar.getMethod() != null &&
icalendar.getMethod().isReply())
return "text/calendar" +
"; method=REPLY" +
"; charset=" + Charset.defaultCharset().name();
} catch (IOException ex) {
Log.e(ex);
}
return attachment.type;
}
@Override
public String getContentType(String filename) {
return attachment.type;
return getContentType(new File(filename));
}
});
bpAttachment.setDataHandler(new DataHandler(dataSource));
bpAttachment.setFileName(attachment.name);
if (attachment.disposition != null)
bpAttachment.setDisposition(attachment.disposition);
if (attachment.cid != null)

View File

@ -71,12 +71,51 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvCalendarEnd" />
<Button
android:id="@+id/btnCalendarAccept"
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="12dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/title_icalendar_accept"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAttendees" />
<Button
android:id="@+id/btnCalendarDecline"
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/title_icalendar_decline"
app:layout_constraintStart_toEndOf="@id/btnCalendarAccept"
app:layout_constraintTop_toBottomOf="@id/tvAttendees" />
<Button
android:id="@+id/btnCalendarMaybe"
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/title_icalendar_maybe"
app:layout_constraintStart_toEndOf="@id/btnCalendarDecline"
app:layout_constraintTop_toBottomOf="@id/tvAttendees" />
<View
android:id="@+id/paddingBottom"
android:layout_width="wrap_content"
android:layout_height="6dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAttendees" />
app:layout_constraintTop_toBottomOf="@id/btnCalendarMaybe" />
<eu.faircode.email.ContentLoadingProgressBar
android:id="@+id/pbCalendarWait"
@ -95,4 +134,10 @@
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="vSeparatorCalendar,paddingBottom" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpCalendarResponse"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="btnCalendarAccept,btnCalendarDecline,btnCalendarMaybe" />
</androidx.constraintlayout.widget.ConstraintLayout>