forked from Apoc70/TransportAgent2010
-
Notifications
You must be signed in to change notification settings - Fork 0
/
MessageModify.cs
387 lines (326 loc) · 16.3 KB
/
MessageModify.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
// AttachmentModify
// ----------------------------------------------------------
// Example for intercepting email messages in an Exchange 2010 transport queue
//
// The example intercepts messages sent from a configurable email address(es)
// and checks the mail message for attachments have filename in to format
//
// WORKBOOK_{GUID}
//
// Changing the filename of the attachments makes it easier for the information worker
// to identify the reports in the emails and in the file system as well.
// Copyright (c) Thomas Stensitzki
// ----------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;
// the lovely Exchange
using Microsoft.Exchange.Data.Transport;
using Microsoft.Exchange.Data.Transport.Smtp;
using Microsoft.Exchange.Data.Transport.Email;
using Microsoft.Exchange.Data.Transport.Routing;
namespace SFTools.Messaging.AttachmentModify
{
#region Message Modifier Factory
/// <summary>
/// Message Modifier Factory
/// </summary>
public class MessageModifierFactory : RoutingAgentFactory
{
/// <summary>
/// Instance of our transport agent configuration
/// This is for a later implementation
/// </summary>
private MessageModifierConfig messageModifierConfig = new MessageModifierConfig();
/// <summary>
/// Returns an instance of the agent
/// </summary>
/// <param name="server">The SMTP Server</param>
/// <returns>The Transport Agent</returns>
public override RoutingAgent CreateAgent(SmtpServer server)
{
return new MessageModifier(messageModifierConfig);
}
}
#endregion
#region Message Modifier Routing Agent
/// <summary>
/// The Message Modifier Routing Agent for modifying an email message
/// </summary>
public class MessageModifier : RoutingAgent
{
// The agent uses the fileLock object to synchronize access to the log file
private object fileLock = new object();
/// <summary>
/// The current MailItem the transport agent is handling
/// </summary>
private MailItem mailItem;
/// <summary>
/// This context to allow Exchange to continue processing a message
/// </summary>
private AgentAsyncContext agentAsyncContext;
/// <summary>
/// Transport agent configuration
/// </summary>
private MessageModifierConfig messageModifierConfig;
/// <summary>
/// Constructor for the MessageModifier class
/// </summary>
/// <param name="messageModifierConfig">Transport Agent configuration</param>
public MessageModifier(MessageModifierConfig messageModifierConfig)
{
// Set configuration
this.messageModifierConfig = messageModifierConfig;
// Register an OnRoutedMessage event handler
this.OnRoutedMessage += OnRoutedMessageHandler;
}
/// <summary>
/// Event handler for OnRoutedMessage event
/// </summary>
/// <param name="source">Routed Message Event Source</param>
/// <param name="args">Queued Message Event Arguments</param>
void OnRoutedMessageHandler(RoutedMessageEventSource source, QueuedMessageEventArgs args)
{
lock (fileLock) {
try {
this.mailItem = args.MailItem;
this.agentAsyncContext = this.GetAgentAsyncContext();
// Get the folder for accessing the config file
string dllDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
// Fetch the from address from the current mail item
RoutingAddress fromAddress = this.mailItem.FromAddress;
Boolean boWorkbookFound = false; // We just want to modifiy subjects when we modified an attachement first
#region External Receive Connector Example
// CHeck first, if the mail item does have a ReceiveConnectorName property first to prevent ugly things to happen
if (mailItem.Properties.ContainsKey("Microsoft.Exchange.Transport.ReceiveConnectorName")) {
// This is just an example, if you want to do something with a mail item which has been received via a named external receive connector
if (mailItem.Properties["Microsoft.Exchange.Transport.ReceiveConnectorName"].ToString().ToLower() == "externalreceiveconnectorname")
{
// do something fancy with the email
}
}
#endregion
RoutingAddress catchAddress;
// Check, if we have any email addresses configured to look for
if (this.messageModifierConfig.AddressMap.Count > 0) {
// Now lets check, if the sender address can be found in the dictionary
if (this.messageModifierConfig.AddressMap.TryGetValue(fromAddress.ToString().ToLower(), out catchAddress)) {
// Sender address found, now check if we have attachments to handle
if (this.mailItem.Message.Attachments.Count != 0) {
// Get all attachments
AttachmentCollection attachments = this.mailItem.Message.Attachments;
// Modify each attachment
for (int count = 0; count < this.mailItem.Message.Attachments.Count; count++) {
// Get attachment
Attachment attachment = this.mailItem.Message.Attachments[count];
// We will only transform attachments which start with "WORKBOOK_"
if (attachment.FileName.StartsWith("WORKBOOK_")) {
// Create a new filename for the attachment
// [MODIFIED SUBJECT]-[NUMBER].[FILEEXTENSION]
String newFileName = MakeValidFileName(string.Format("{0}-{1}{2}", ModifiySubject(this.mailItem.Message.Subject.Trim()), count + 1, Path.GetExtension(attachment.FileName)));
// Change the filename of the attachment
this.mailItem.Message.Attachments[count].FileName = newFileName;
// Yes we have changed the attachment. Therefore we want to change the subject as well.
boWorkbookFound = true;
}
}
// Have changed any attachments?
if (boWorkbookFound) {
// Then let's change the subject as well
this.mailItem.Message.Subject = ModifiySubject(this.mailItem.Message.Subject);
}
}
}
}
}
catch (System.IO.IOException ex) {
// oops
Debug.WriteLine(ex.ToString());
this.agentAsyncContext.Complete();
}
finally {
// We are done
this.agentAsyncContext.Complete();
}
}
// Return to pipeline
return;
}
/// <summary>
/// Build a new subject, if the first 10 chars of the original subject are a valid date.
/// We muste transform the de-DE format dd.MM.yyyy to yyyyMMdd for better sortability in the email client.
/// </summary>
/// <param name="MessageSubject">The original subject string</param>
/// <returns>The modified subject string, if modification was possible</returns>
private static string ModifiySubject(string MessageSubject)
{
string newSubject = String.Empty;
if (MessageSubject.Length >= 10) {
string dateCheck = MessageSubject.Substring(0, 10);
DateTime dt = new DateTime();
try {
// Check if we can parse the datetime
if (DateTime.TryParse(dateCheck, out dt)) {
// lets fetch the subject starting at the 10th character
string subjectRight = MessageSubject.Substring(10).Trim();
// build a new subject
newSubject = string.Format("{0:yyyyMMdd} {1}", dt, subjectRight);
}
}
finally {
// do nothing
}
}
return newSubject;
}
/// <summary>
/// Replace invalid filename chars with an underscore
/// </summary>
/// <param name="name">The filename to be checked</param>
/// <returns>The sanitized filename</returns>
private static string MakeValidFileName(string name)
{
string invalidChars = Regex.Escape(new string(Path.GetInvalidFileNameChars()));
string invalidRegExStr = string.Format(@"[{0}]+", invalidChars);
return Regex.Replace(name, invalidRegExStr, "_");
}
}
#endregion
#region Message Modifier Configuration
/// <summary>
/// Message Modifier Configuration class
/// </summary>
public class MessageModifierConfig
{
/// <summary>
/// The name of the configuration file.
/// </summary>
private static readonly string configFileName = "SFTools.MessageModify.Config.xml";
/// <summary>
/// Point out the directory with the configuration file (= assembly location)
/// </summary>
private string configDirectory;
/// <summary>
/// The filesystem watcher to monitor configuration file updates.
/// </summary>
private FileSystemWatcher configFileWatcher;
/// <summary>
/// The from address
/// </summary>
private Dictionary<string, RoutingAddress> addressMap;
/// <summary>
/// Whether reloading is ongoing
/// </summary>
private int reLoading = 0;
/// <summary>
/// The mapping between domain to catchall address.
/// </summary>
public Dictionary<string, RoutingAddress> AddressMap
{
get { return this.addressMap; }
}
/// <summary>
/// Constructor
/// </summary>
public MessageModifierConfig()
{
// Setup a file system watcher to monitor the configuration file
this.configDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
this.configFileWatcher = new FileSystemWatcher(this.configDirectory);
this.configFileWatcher.NotifyFilter = NotifyFilters.LastWrite;
this.configFileWatcher.Filter = configFileName;
this.configFileWatcher.Changed += new FileSystemEventHandler(this.OnChanged);
// Create an initially empty map
this.addressMap = new Dictionary<string, RoutingAddress>();
// Load the configuration
this.Load();
// Now start monitoring
this.configFileWatcher.EnableRaisingEvents = true;
}
/// <summary>
/// Configuration changed handler.
/// </summary>
/// <param name="source">Event source.</param>
/// <param name="e">Event arguments.</param>
private void OnChanged(object source, FileSystemEventArgs e)
{
// Ignore if load ongoing
if (Interlocked.CompareExchange(ref this.reLoading, 1, 0) != 0) {
Trace.WriteLine("load ongoing: ignore");
return;
}
// (Re) Load the configuration
this.Load();
// Reset the reload indicator
this.reLoading = 0;
}
/// <summary>
/// Load the configuration file. If any errors occur, does nothing.
/// </summary>
private void Load()
{
// Load the configuration
XmlDocument doc = new XmlDocument();
bool docLoaded = false;
string fileName = Path.Combine(this.configDirectory, MessageModifierConfig.configFileName);
try {
doc.Load(fileName);
docLoaded = true;
}
catch (FileNotFoundException) {
Trace.WriteLine("Configuration file not found: {0}", fileName);
}
catch (XmlException e) {
Trace.WriteLine("XML error: {0}", e.Message);
}
catch (IOException e) {
Trace.WriteLine("IO error: {0}", e.Message);
}
// If a failure occured, ignore and simply return
if (!docLoaded || doc.FirstChild == null) {
Trace.WriteLine("Configuration error: either no file or an XML error");
return;
}
// Create a dictionary to hold the mappings
Dictionary<string, RoutingAddress> map = new Dictionary<string, RoutingAddress>(100);
// Track whether there are invalid entries
bool invalidEntries = false;
// Validate all entries and load into a dictionary
foreach (XmlNode node in doc.FirstChild.ChildNodes) {
if (string.Compare(node.Name, "domain", true, CultureInfo.InvariantCulture) != 0) {
continue;
}
XmlAttribute domain = node.Attributes["name"];
XmlAttribute address = node.Attributes["address"];
// Validate the data
if (domain == null || address == null) {
invalidEntries = true;
Trace.WriteLine("Reject configuration due to an incomplete entry. (Either or both domain and address missing.)");
break;
}
if (!RoutingAddress.IsValidAddress(address.Value)) {
invalidEntries = true;
Trace.WriteLine(String.Format("Reject configuration due to an invalid address ({0}).", address));
break;
}
// Add the new entry
string lowerDomain = domain.Value.ToLower();
map[lowerDomain] = new RoutingAddress(address.Value);
Trace.WriteLine(String.Format("Added entry ({0} -> {1})", lowerDomain, address.Value));
}
// If there are no invalid entries, swap in the map
if (!invalidEntries) {
Interlocked.Exchange<Dictionary<string, RoutingAddress>>(ref this.addressMap, map);
Trace.WriteLine("Accepted configuration");
}
}
}
#endregion
}