Skip to content
This repository has been archived by the owner on Feb 16, 2023. It is now read-only.

Commit

Permalink
- background thread for scanning new mails and apply rules (Fixes #2)
Browse files Browse the repository at this point in the history
- add another quit event handler (addin one is triggered in special cases)
  • Loading branch information
cristi993 committed Aug 31, 2018
1 parent 4d35329 commit 2686800
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 97 deletions.
132 changes: 41 additions & 91 deletions Room17DE.MeetingDecline/AddIn.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Office.Interop.Outlook;
using Room17DE.MeetingDecline.Util;

Expand All @@ -8,18 +10,26 @@ public partial class AddIn
{
private Folders DeletedItemsFolder;
internal static int[] SystemFoldersIDs;
private CancellationTokenSource CancelationTokenSource = new CancellationTokenSource();

private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
// event handler for new email
this.Application.NewMailEx += Application_NewMailEx;
// check if we have settings
if (Properties.Settings.Default.MeetingDeclineRules == null)
Properties.Settings.Default.MeetingDeclineRules = new Dictionary<string, DeclineRule>();

if (Properties.Settings.Default.LastMailCheck == null)
Properties.Settings.Default.LastMailCheck = new Dictionary<string, DateTime>();

Properties.Settings.Default.Save();

// make sure that a deleted folder removes the meetingdecline rule
Folder deletedItemsFolder = (Folder)Application.Session.GetDefaultFolder(OlDefaultFolders.olFolderDeletedItems);
DeletedItemsFolder = deletedItemsFolder.Folders; // keep a reference at class level so it wont be GCed and event handler lost
DeletedItemsFolder.FolderAdd += DeletedItems_FolderAdd;

// enumerate non user folder entry ids for later
#region hashes
Array systemFolders = Enum.GetValues(typeof(OlDefaultFolders));
string[] customFolders = new string[] { "Yammer Root", "Files", "Conversation History", "Social Activity Notifications", "Scheduled", "Quick Step Settings", "Archive", "Conversation Action Settings" };
SystemFoldersIDs = new int[systemFolders.Length + customFolders.Length];
Expand All @@ -38,6 +48,13 @@ private void ThisAddIn_Startup(object sender, System.EventArgs e)
.Folders[customFolders[i - systemFolders.Length]].EntryID.GetHashCode();
}
catch { } // being a hardcoded list, we can't be 100% sure it always exists from app to app
#endregion

// timer thread for handling new mails in folders
NewMailPeriodicTask.Run(new TimeSpan(0, 1 ,0), CancelationTokenSource.Token); // TODO: make interval configurable?

// register to process exit event for cleanup
AppDomain.CurrentDomain.ProcessExit += Addin_ProcessExit;
}

/// <summary>
Expand All @@ -46,117 +63,50 @@ private void ThisAddIn_Startup(object sender, System.EventArgs e)
/// </summary>
private void DeletedItems_FolderAdd(MAPIFolder Folder)
{
// check if we have settings
if (Properties.Settings.Default.MeetingDeclineRules == null)
return;

// check if folder exists in settings, then remove it and save
if (Properties.Settings.Default.MeetingDeclineRules.ContainsKey(Folder.EntryID))
{
Properties.Settings.Default.MeetingDeclineRules.Remove(Folder.EntryID);
Properties.Settings.Default.Save();
}

// same for checked map too
if (Properties.Settings.Default.LastMailCheck.ContainsKey(Folder.EntryID))
{
Properties.Settings.Default.LastMailCheck.Remove(Folder.EntryID);
Properties.Settings.Default.Save();
}
}

// TODO: replace NewEx with something else to really handle all new emails; https://www.add-in-express.com/creating-addins-blog/2011/11/10/outlook-newmail-custom-solution/ ?
/// <summary>
/// Event handler for every new email received, regardless of its type
/// Activates this application Ribbon
/// </summary>
private void Application_NewMailEx(string EntryIDCollection)
protected override Microsoft.Office.Core.IRibbonExtensibility CreateRibbonExtensibilityObject() => new Ribbon();

#region shutdown
private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
{
// check if we have settings
if (Properties.Settings.Default.MeetingDeclineRules == null)
return;

// get the meeting, if it's a meeting
MeetingItem meetingItem = GetMeeting(EntryIDCollection);
if (meetingItem == null)
return;

// get current meeting parent folder
if (!(meetingItem.Parent is MAPIFolder parentFolder)) return;

// check if parent folder is between settings
if(Properties.Settings.Default.MeetingDeclineRules.ContainsKey(parentFolder.EntryID))
{
// check if rule it's active
DeclineRule rule = Properties.Settings.Default.MeetingDeclineRules[parentFolder.EntryID];
if (rule.IsActive)
{
// if it's a Cancelation, delete it from calendar
if (meetingItem.Class == OlObjectClass.olMeetingCancellation)
{
if (meetingItem.GetAssociatedAppointment(false) != null)
{ meetingItem.GetAssociatedAppointment(false).Delete(); return; }
meetingItem.Delete(); return; // if deleted by user/app, delete the whole message
}

// get associated appointment
AppointmentItem appointment = meetingItem.GetAssociatedAppointment(false);
string globalAppointmentID = appointment.GlobalAppointmentID;

// optional, send notification back to sender
appointment.ResponseRequested &= rule.SendNotification;

// set decline to the meeting
MeetingItem responseMeeting = appointment.Respond(rule.Response, true);
// https://msdn.microsoft.com/en-us/VBA/Outlook-VBA/articles/appointmentitem-respond-method-outlook
// says that Respond() will return a new meeting object for Tentative response

// optional, add a meesage to the Body
if (!String.IsNullOrEmpty(rule.Message))
(responseMeeting ?? meetingItem).Body = rule.Message;

// send decline
//if(rule.Response == OlMeetingResponse.olMeetingDeclined)
(responseMeeting ?? meetingItem).Send();

// and delete the appointment if tentative
if (rule.Response == OlMeetingResponse.olMeetingTentative)
appointment.Delete();

// after Sending the response, sometimes the appointment doesn't get deleted from calendar,
// but appointmnent could become and invalid object, so we need to search for it and delete it
AppointmentItem newAppointment = this.Application.Session.GetDefaultFolder(OlDefaultFolders.olFolderCalendar).Items
.Find("@SQL=\"http://schemas.microsoft.com/mapi/id/{6ED8DA90-450B-101B-98DA-00AA003F1305}/00030102\" = '"
+ globalAppointmentID + "' ");
if (newAppointment != null)
newAppointment.Delete();
}
}
// Note: Outlook no longer raises this event. If you have code that
// must run when Outlook shuts down, see https://go.microsoft.com/fwlink/?LinkId=506785
AddinShutdown();
}

/// <summary>
/// Get a MeetingItem based on EntryIDCollection, or null if it's not a meeting
/// Event handler for AppDomain process exit
/// </summary>
/// <param name="EntryIDCollection">The ID of the meeting</param>
/// <returns>A MeetingItem that corresponds to the EntryID</returns>
internal MeetingItem GetMeeting(string EntryIDCollection)
private void Addin_ProcessExit(object sender, EventArgs e)
{
object item = null;
try
{
item = Globals.AddIn.Application.Session.GetItemFromID(EntryIDCollection);
}
catch
{
return null;
}

MeetingItem meetingItem = item as MeetingItem;
return meetingItem;
AddinShutdown();
}

/// <summary>
/// Activates this application Ribbon
/// Method that needs to be called when Outlook is shutting down
/// </summary>
protected override Microsoft.Office.Core.IRibbonExtensibility CreateRibbonExtensibilityObject() => new Ribbon();

private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
private void AddinShutdown()
{
// Note: Outlook no longer raises this event. If you have code that
// must run when Outlook shuts down, see https://go.microsoft.com/fwlink/?LinkId=506785
CancelationTokenSource.Cancel();
}
#endregion

#region VSTO generated code

Expand Down
4 changes: 0 additions & 4 deletions Room17DE.MeetingDecline/Forms/RulesForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

Expand Down Expand Up @@ -94,9 +93,6 @@ private async Task<bool> LoadRules()
{
try
{
// read settings
if (Room17DE.MeetingDecline.Properties.Settings.Default.MeetingDeclineRules == null)
Room17DE.MeetingDecline.Properties.Settings.Default.MeetingDeclineRules = new Dictionary<string, DeclineRule>();
Rules = Room17DE.MeetingDecline.Properties.Settings.Default.MeetingDeclineRules;
// get all folders
Expand Down
3 changes: 2 additions & 1 deletion Room17DE.MeetingDecline/Room17DE.MeetingDecline.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<PublishUrl>publish\</PublishUrl>
<InstallUrl />
<TargetCulture>en</TargetCulture>
<ApplicationVersion>1.0.0.4</ApplicationVersion>
<ApplicationVersion>1.0.0.5</ApplicationVersion>
<AutoIncrementApplicationRevision>true</AutoIncrementApplicationRevision>
<UpdateEnabled>true</UpdateEnabled>
<UpdateInterval>7</UpdateInterval>
Expand Down Expand Up @@ -201,6 +201,7 @@
<Compile Include="Ribbon.cs" />
<Compile Include="Settings.cs" />
<Compile Include="Util\DeclineRule.cs" />
<Compile Include="Util\NewMailPeriodicTask.cs" />
<EmbeddedResource Include="Forms\DeclineMessageForm.resx">
<DependentUpon>DeclineMessageForm.cs</DependentUpon>
</EmbeddedResource>
Expand Down
10 changes: 10 additions & 0 deletions Room17DE.MeetingDecline/Settings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Room17DE.MeetingDecline.Util;
using System;
using System.Collections.Generic;
using System.Configuration;

Expand Down Expand Up @@ -30,6 +31,15 @@ public IDictionary<string, DeclineRule> MeetingDeclineRules
set { this[nameof(MeetingDeclineRules)] = value; }
}

[UserScopedSetting]
[SettingsSerializeAs(SettingsSerializeAs.Binary)]
[DefaultSettingValue("")]
public IDictionary<string, DateTime> LastMailCheck
{
get { return (IDictionary<string, DateTime>)this[nameof(LastMailCheck)]; }
set { this[nameof(LastMailCheck)] = value; }
}

private void SettingChangingEventHandler(object sender, System.Configuration.SettingChangingEventArgs e) {
// Add code to handle the SettingChangingEvent event here.
}
Expand Down
145 changes: 145 additions & 0 deletions Room17DE.MeetingDecline/Util/NewMailPeriodicTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using Microsoft.Office.Interop.Outlook;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Room17DE.MeetingDecline.Util
{
static class NewMailPeriodicTask
{
public static async Task Run(TimeSpan period, CancellationToken cancellationToken)
{
// keep scanning for new mails while thread is not stopped
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(period, cancellationToken);

if (!cancellationToken.IsCancellationRequested)
CheckNewEmails(cancellationToken);
}
}

/// <summary>
/// Starting point of check and apply decline rules for new mails
/// </summary>
/// <param name="cancellationToken">A cancelation token to stop this method from executing in an async context</param>
private static void CheckNewEmails(CancellationToken cancellationToken)
{
bool save = false;

// get folders from rules
IDictionary<string, DeclineRule> rules = Properties.Settings.Default.MeetingDeclineRules;
IDictionary<string, DateTime> lastCheckedFolders = Properties.Settings.Default.LastMailCheck;

foreach (string folderID in rules.Keys)
{
// skip inactive rules
if (!rules[folderID].IsActive)
continue;

// get folder
MAPIFolder folder = Globals.AddIn.Application.Session.GetFolderFromID(folderID);
if (folder == null)
continue; // user deleted folder

// get last date when new mails were checked
DateTime lastCheckedDate = DateTime.MinValue;
if (lastCheckedFolders.ContainsKey(folderID))
lastCheckedDate = lastCheckedFolders[folderID];

// make sure to not check last mail twice
if (lastCheckedDate != DateTime.MinValue)
lastCheckedDate = lastCheckedDate.AddMinutes(+1);

// stop expensive ephemeral execution
if (cancellationToken.IsCancellationRequested)
return;

// get new emails
folder.Items.Sort("[ReceivedTime]");
Items results = folder.Items.Restrict(String.Format("[ReceivedTime] >= '{0}' AND [Unread]=true",
lastCheckedDate.ToString("g")));
DateTime? lastDate = null;

foreach (object item in results)
{
// get only meetings
if (!(item is MeetingItem meetingItem))
continue;

// save date
lastDate = meetingItem.ReceivedTime;

// stop expensive ephemeral execution
if (cancellationToken.IsCancellationRequested)
return;

// process meeting rule
try { ProcessRule(meetingItem, rules[folderID]); }
catch { continue; }
}

// update latest mail entry for this folder
if (lastDate != null)
{
lastCheckedFolders[folderID] = (DateTime)lastDate;
save = true;
}
}

// save new last date
if (save)
Properties.Settings.Default.Save();
}

/// <summary>
/// Applies the appropiate rule to the meeting parameter
/// </summary>
/// <param name="meetingItem">The meeting that needs processing</param>
/// <param name="rule">the associated decline rule for the folder of this meetingItem</param>
private static void ProcessRule(MeetingItem meetingItem, DeclineRule rule)
{
// if it's a Cancelation, delete it from calendar
if (meetingItem.Class == OlObjectClass.olMeetingCancellation)
{
if (meetingItem.GetAssociatedAppointment(false) != null)
{ meetingItem.GetAssociatedAppointment(false).Delete(); return; }
meetingItem.Delete(); return; // if deleted by user/app, delete the whole message
}

// get associated appointment
AppointmentItem appointment = meetingItem.GetAssociatedAppointment(false);
string globalAppointmentID = appointment.GlobalAppointmentID;

// optional, send notification back to sender
appointment.ResponseRequested &= rule.SendNotification;

// set decline to the meeting
MeetingItem responseMeeting = appointment.Respond(rule.Response, true);
// https://msdn.microsoft.com/en-us/VBA/Outlook-VBA/articles/appointmentitem-respond-method-outlook
// says that Respond() will return a new meeting object for Tentative response

// optional, add a meesage to the Body
if (!String.IsNullOrEmpty(rule.Message))
(responseMeeting ?? meetingItem).Body = rule.Message;

// send decline
//if(rule.Response == OlMeetingResponse.olMeetingDeclined)
(responseMeeting ?? meetingItem).Send();

// and delete the appointment if tentative
if (rule.Response == OlMeetingResponse.olMeetingTentative)
appointment.Delete();

// after Sending the response, sometimes the appointment doesn't get deleted from calendar,
// but appointmnent could become and invalid object, so we need to search for it and delete it
AppointmentItem newAppointment = Globals.AddIn.Application.Session.GetDefaultFolder(OlDefaultFolders.olFolderCalendar).Items
.Find("@SQL=\"http://schemas.microsoft.com/mapi/id/{6ED8DA90-450B-101B-98DA-00AA003F1305}/00030102\" = '"
+ globalAppointmentID + "' ");
if (newAppointment != null)
newAppointment.Delete();

}
}
}
3 changes: 2 additions & 1 deletion Room17DE.MeetingDecline/app.config
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
<configuration>
<configSections>
</configSections>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/></startup></configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/></startup>
</configuration>

0 comments on commit 2686800

Please sign in to comment.