Problem:
- User wants to be able to receive email notifications from SharePoint Discussion Board.
- User should be able to start & reply threads via email, using his Outlook or mobile phone.
- Ideally, replies should go into the Correct Topic Folder and new threads should create a New Topic Folder in SharePoint Discussion Board.
At first, I thought “Dude, this is Microsoft. They must have think about this functionality”. So I accomplished Problem Number 1 using SharePoint Alert feature. For Problem Number 2, I used the Incoming Email feature in List Settings. Done?
No.
You see, if you use the Alert feature, SharePoint sends the email from the email you configure in Central Admin. What this means is that when user hits reply, they now have to remember the Discussion Email Address configured in List Settings Incoming Email !
Just for fun, I cheekily asked the business user, “What’s wrong with that? You will always remember our hostname after @, and just give a short-name for the alias before @” 😀
Of course I get the reply, “Not acceptable. Find a solution. This is Microsoft product, if lacking the feature, clearly there should be 3rd party solutions out there that fill the gap.”
So I went on a 1-week research about Discussion Board add-ons. I had a look at Lightning Tools; their Social Squared supports emailing threads, but alas it’s built on top of SQL Database, meaning a separate web app, not a SharePoint solution. Their Storm Forum is based on SharePoint list, but according to the feature matrix, no email support. Kwizcom has another SharePoint solution, but again, no email support 🙁
What does this mean? That means I gotta code my own SharePoint customization. Yay! My technical heart pumps in excitement. Not so for the people who depended on me for other stuff: “How long will it take you to implement this custom solution?” 2-3 weeks I estimated. 3 days it took me. And now that I will give you the solution, 1 day for you oh readers 😀
Let me give you a peek of what this customization can do:
From the screenshots above, you can see that I accomplished Problem no. 3 too: showing emails in Threaded View. This is no easy feature. After dissecting Microsoft.SharePoint.dll using ILSpy, I finally discovered the class SPDiscussionEmailHandler.cs. In that file, there is a method named GetFolder() that retrieves the folder of the Discussion Topic.
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 |
private SPListItem GetFolder(SPDiscussionEmailHandler.MessageContainer container, out SPListItemCollection can { // removed non-essential codes . . . foreach (SPListItem sPListItem in candidates) { string index = (string)sPListItem["ThreadIndex"]; string topic = (string)sPListItem["ThreadTopic"]; if (SPDiscussionEmailHandler.SameThread(headers.threadIndexHeader, headers.threadTopic, index, topic)) { SPListItem result = sPListItem; return result; } string text = (string)sPListItem["RelevantMessages"]; if (!string.IsNullOrEmpty(text)) { if (SPUtility.StsBinaryCompareIndexOf(text, messageId) > -1) { SPListItem result = sPListItem; return result; } string[] array = references; for (int i = 0; i < array.Length; i++) { string str = array[i]; if (SPUtility.StsBinaryCompareIndexOf(text, str) > -1) { SPListItem result = sPListItem; return result; } } } } return null; } |
The above code shows that when receiving Incoming Email, SharePoint decides a reply is part of the same Topic Thread if:
- It has the same Thread-Topic and Thread-Index email headers
- The RelevantMessages column of ListItem contains the original Message-ID of the Topic Folder.
I thought I could accomplish this by using Thread-Topic and Thread-Index, but Outlook strangely removes the Thread-Topic email header when I clicked Reply!
Thankfully there is that 2nd option using the RelevantMessages field 😀
So finally, lo and behold below is the Custom Event Receiver that you should attach to List Template ID of 108 (SharePoint Discussion Board List)
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 |
public class NewPostHandler : SPItemEventReceiver { private Dictionary<string, string> _memberEmails = new Dictionary<string, string>(); private Guid _siteID; private Guid _webID; private Guid _listID; private Guid _itemID; public override void ItemAdded(SPItemEventProperties properties) { // Finish whatever base has to do base.ItemAdded(properties); _siteID = properties.SiteId; _webID = properties.Web.ID; _listID = properties.ListId; _itemID = properties.ListItem.UniqueId; // We are going to Sleep // better do it on another thread var thread = new System.Threading.Thread(ItemAddedQueue); thread.Start(); } public void ItemAddedQueue() { try { // If fired off from Timer Job due to incoming email // need to wait for SPDiscussionEmailHandler to finish // hence longer Sleep time System.Threading.Thread.Sleep(TimeSpan.FromSeconds(20)); using (SPSite site = new SPSite(_siteID)) { using (SPWeb web = site.OpenWeb(_webID)) { SPList list = web.Lists[_listID]; SPListItem item = list.GetItemByUniqueId(_itemID); // Prevent ArgumentNullException due to ListItem not fully filled with info // happens if fired from Timer Job if (web.GetFile(item.Url).Exists) { item = web.GetFile(item.Url).Item; } else if (web.GetFolder(item.Url).Exists) { item = web.GetFolder(item.Url).Item; } #region 1. Build the Members Email List _memberEmails.Clear(); // clear in case member permission removed before next run foreach (SPRoleAssignment ra in list.RoleAssignments) { // Ignore Groups intentionally if (ra.Member is SPUser) { if (ra.Member.LoginName.ToUpper() == "SHAREPOINT\\system".ToUpper()) continue; // skip system account SPUser user = (SPUser)ra.Member; if (!_memberEmails.ContainsKey(user.LoginName) && !string.IsNullOrEmpty(user.Email)) { _memberEmails[user.LoginName] = user.Email; } } } #endregion #region 2. Build the Email Body var msg = new MailMessage(); var emailAlias = list.EmailAlias; var incomingEmailService = SPFarm.Local.Services.GetValue<SPIncomingEmailService>(); var emailHost = incomingEmailService.ServerDisplayAddress; msg.From = new MailAddress(string.Format("{0}@{1}", emailAlias, emailHost)); msg.IsBodyHtml = true; string threadLink; string threadTopic; string messageId = null; string threadIndex; string subject; string action; var folder = web.GetFile(item.Url).ParentFolder; if (folder != null && folder.Name == list.RootFolder.Name) { // New Discussion action = "started"; threadLink = string.Format("{0}/{1}", web.Url, item.Url); threadTopic = item.Title; subject = threadTopic; messageId = item["MessageId"].ToString(); threadIndex = item["ThreadIndex"].ToString(); } else { // Reply action = "replied"; threadTopic = folder.Name; subject = string.Format("RE: {0}", threadTopic); threadLink = string.Format("{0}/{1}", web.Url, folder.Url); var parentItem = list.GetItemByUniqueId(folder.UniqueId); messageId = parentItem["MessageId"].ToString(); threadIndex = parentItem["ThreadIndex"].ToString(); } msg.Subject = subject; string postedBy = string.Empty; string author; if (item.Fields.Contains(SPBuiltInFieldId.Created_x0020_By)) { // Incoming email via Timer Job // always give System Account but Created By gives the matching user author = item[SPBuiltInFieldId.Created_x0020_By].ToString(); } else { // Adding/Replying via Web, Created By is null, need to use Author author = item[SPBuiltInFieldId.Author].ToString(); } var authorValue = new SPFieldUserValue(web, author); if (authorValue != null) postedBy = authorValue.User.Name; msg.Body = string.Format("{0} {1} :\r\n\r\n{2}\r\nView the whole thread at <a href='{3}'>{3}</a>", postedBy, action, item[SPBuiltInFieldId.Body], threadLink); // Add the Thread-Index headers msg.Headers.Add("Thread-Topic", threadTopic); threadIndex = threadIndex.Substring(2); byte[] byteArray = FromHex(threadIndex); string threadIndexEncoded = base64Encode(byteArray); msg.Headers.Add("Thread-Index", threadIndexEncoded); msg.Headers.Add("Message-ID", messageId); // update RelevantMessage field item["RelevantMessages"] = messageId; item.Update(); #endregion #region 3. Send Email to each member string smtpServer = SPAdministrationWebApplication.Local.OutboundMailServiceInstance.Server.Address; var smtp = new SmtpClient(smtpServer); foreach (var kvp in _memberEmails) { msg.To.Clear(); msg.To.Add(new MailAddress(kvp.Value)); smtp.Send(msg); } #endregion } } } catch (Exception ex) { PortalLog.LogString("DiscussionBoardSendEmail : " + ex.ToString()); } } public static byte[] FromHex(string hex) { hex = hex.Replace("-", ""); byte[] raw = new byte[hex.Length / 2]; for (int i = 0; i < raw.Length; i++) { raw[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16); } return raw; } public static string base64Encode(byte[] data) { try { byte[] encData_byte = data; string encodedData = Convert.ToBase64String(encData_byte); return encodedData; } catch (Exception e) { throw new Exception("Error in base64Encode" + e.Message); } } } |
Leave a comment if you like this solution 😀
Superrrr !!!!
Gr8 Article
coool
Hi Zeddy,
Thanks for your wonderful article. it was a great releif to have a custom feature so that the discussion can sit on the correct thread whenever they are getting Created/Replied from email.
I used the above feature. i have few questions about it.
when I create a new item from the Discussion board (NOT Email) itself or reply to an existing discussion from Discussion board (NOT Email), the message id and thread index is giving exception
messageId = item[“MessageId”].ToString();
OR
messageId = parentItem[“MessageId”].ToString();
what is your thought about it.
Regards,
Yogendra
@yogendra: hi Yogendra, strange, I am never getting MessageID null. Further creating new posts replying via Web will immediately hit the code without waiting for SPDiscussionEmailHandler to finish first. Set breakpoints on your ItemAdded event receiver and see what happens (remember it is not ItemAdding).
Hi,
I”m a bit late at the party, but I do have a question:
Can this be used to reply to sharepoint discussion threads using ANY e-mail client/webmail so long as the e-mail address is linked to a registered sharepoint user with rights to post into the respective thread?
Thanks in advance!
@andrew: Yes, I tested using iPhone, BlackBerry, Windows Phone and Outlook. As long as you click reply without modifying the email headers the replies will be threaded automatically in SP2010.
Great article Zeddy.
As the previous poster noted, I know I’m a bit late to the thread. Your thread is one of the de facto posts on how to do this, and I hope it still fits in your day to be able to reply to comments.
I’m replying to discussion board via Yahoo and Gmail web interfaces. To me, it seems both change the Message-ID and both are also stripping the Thread-Index and Thread-Topic headers.
Have you had any success with these means of replying and/or know how to get them to work?
Thanks in advance.
Hi,
Nice article.
Can we send the entire reply along with the one newly added reply in the thread through email, and is there anyway to attach attachments ?
Thanks
Ravish
Wow You are a life saver
Hi I have implemented this in one of my works for discussion works like magic.. but i found out that when user edits the topic subject from the SP UI then it creates a new thread from reply mail
Great!! Zeddy
hi I’m quite new to sharepoint. Could you leave a few helpful links that could guide me, so I can follow your post step by step?
Thank You. Great article. Very useful and saved lot of time.
You achieved Problem #3 using custom event receiver, but how you solved problem #2 (User should be able to start & reply threads via email, using his Outlook or mobile phone.)
“If you use the Alert feature, SharePoint sends the email from the email you configure in Central Admin. What this means is that when user hits reply, they now have to remember the Discussion Email Address configured in List Settings Incoming Email!”
How users reply back to an email address configured in Discussion Board (Incoming Email), when they received an email alert from different address?
Regards,
Arkay
Super late to the party….would this work for discussion lists from categories?..It’s part of the community set from sharepoint 2013