Share via


How to: Customize E-Mail for Project Server Notifications

This content is outdated and is no longer being maintained. It is provided as a courtesy for individuals who are still using these technologies. This page may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist.

The Notifications methods in Microsoft Office Project Server 2007 allow you to read and update notification settings for resources. However, this method does not provide a way to use custom XSLT transformation files for modifying e-mail messages from the notifications system. As a custom workaround, you can trap the OnSending pre-event, transform a specific alert or reminder with a custom XSLT transformation file, send the modified e-mail message, and then cancel the e-mail message from the Project Server notifications system. (The code in this article is adapted from test code by Cosmin Catrinescu, Microsoft Corporation.)

The event arguments for the OnSending event include all the information you need to determine the recipient, e-mail mode (text or HTML), language, notification type, and body of the message. You can get the recipient's e-mail address from the ReadResources method in the Resource Web service of the Project Server Interface (PSI), and send the customized e-mail message with the SMTP service.

This article includes the following sections:

  • Extracting the Default XSLT Transformation Files

  • Creating the OnSending Event Handler

  • Installing the OnSending Event Handler and Configuring SMTP

  • Testing the OnSending Event Handler

Important noteImportant

Before you deploy the solution to a production server, modify and test the NotificationsEventHandler method and customize the XSLT transformation files you need on a test installation of Project Server.

You can use Microsoft Visual Studio 2005 or later to create the event handler. The Project 2007 SDK download includes the complete Microsoft Visual C# source code for NotificationsEventHandler method plus the 32 default English language XSLT transformation files for alerts and reminders in text and HTML output. For a link to the download, see Welcome to the Microsoft Office Project 2007 SDK.

Extracting the Default XSLT Transformation Files

You can modify the default XSLT transformation files in the SDK download and save them to a directory on the Project Server computer for use by the SendMyNotification method in the event handler. The file names show the e-mail type, mode, and language. For example, the Alert-AssignmentsChanged_Html_1033.xslt file contains the XSLT code that transforms the Alert-AssignmentsChanged notification to an English language HTML e-mail message.

If you have Project Server installed for other languages, you must extract the default XSLT transformation files from the Published database. The NOTIFICATION_EMAIL_TYPE field in the MSP_NOTIFICATION_XSLS table shows the e-mail type GUID for e-mail names such as Alert-AssignmentsChanged and Reminder-StatusReport. The e-mail names are in the MSP_EMAIL_DEFINITION table. To show a list of the e-mail names that correspond with the e-mail type, run the following query on the Published database.

SELECT     MSP_NOTIFICATION_XSLS.NOTIFICATION_EMAIL_TYPE_UID AS EMAIL_TYPE, 
           MSP_NOTIFICATION_XSLS.NOTIFICATION_EMAIL_LCID AS LCID, 
           MSP_NOTIFICATION_XSLS.NOTIFICATION_EMAIL_MODE_ID AS EMAIL_MODE, 
           MSP_EMAIL_DEFINITION.EMAIL_NAME AS EMAIL_NAME
FROM       MSP_NOTIFICATION_XSLS INNER JOIN MSP_EMAIL_DEFINITION
               ON MSP_NOTIFICATION_XSLS.NOTIFICATION_EMAIL_TYPE_UID = 
                   MSP_EMAIL_DEFINITION.EMAIL_TYPE_UID

The NOTIFICATION_EMAIL_XSL field in the MSP_NOTIFICATION_XSLS table is of type ntext. In Microsoft SQL Server 2000, the ntext field does not appear in an ANSI query, so you can use the Data Transformation Services Wizard to extract the XSLT code. The steps in Procedure 1a are for SQL Server 2000; the steps in Procedure 1b are for SQL Server 2005.

Procedure 1a: To extract the XSLT code (SQL Server 2000):

  1. In SQL Server Enterprise Manager, right-click the MSP_NOTIFICATION_XSLS table, click All Tasks, and then click Export Data.

  2. In the DTS Import/Export Wizard, click Next.

  3. Select Microsoft OLE DB Provider for SQL Server as the data source, and then select the Published database for your Project Web Access instance. Click Next.

  4. For the destination, click Text File, and then type or browse for a file name. Click Next.

  5. Click Use a query to specify the data to transfer, and then click Next.

  6. On the page for the SQL statement, click Query Builder. Expand the MSP_NOTIFICATION_XSLS table, and move the NOTIFICATION_EMAIL_TYPE_UID, NOTIFICATION_EMAIL_LCID, NOTIFICATION_EMAIL_MODE, and NOTIFICATION_EMAIL_XSL fields to the Selected columns list. Click Next.

  7. On the Specify Sort Order page, click Next. You do not need a sort order.

  8. On the Specify Query Criteria page, leave All rows selected, and then click Next.

  9. The query statement should be the following. Verify it, and then click Next.

    select [MSP_NOTIFICATION_XSLS].[NOTIFICATION_EMAIL_TYPE_UID], 
           [MSP_NOTIFICATION_XSLS].[NOTIFICATION_EMAIL_LCID], 
           [MSP_NOTIFICATION_XSLS].[NOTIFICATION_EMAIL_MODE_ID], 
           [MSP_NOTIFICATION_XSLS].[NOTIFICATION_EMAIL_XSL]
    from [MSP_NOTIFICATION_XSLS]
    
  10. On the Select destination file format page, click Delimited. Select the following values, and then click Next:

    File type: ANSI

    Row delimiter: {CR}{LF}

    Column delimiter: Vertical Bar

    Text qualifier: <none>

  11. Check Run immediately. Click Next, and then click Finish to extract the data.

  12. Open the output text file in Visual Studio, and then copy the XSLT code in each record to a separate XSLT transformation file. The NOTIFICATION_EMAIL_MODE_ID is 0 for text and 1 for HTML. Determine the file name from the e-mail type GUID and the previous query you did on the Published database. For example, the first record begins with the following line:

    {AB66E16A-BE87-41C4-9811-0E9196437102}|1033|0|

    Therefore, the file name for that XSLT record is Reminder-StatusReport_Text_1033.xslt.

Procedure 1b: To extract the XSLT code (SQL Server 2005):

  1. In SQL Server Management Studio, right-click the MSP_NOTIFICATION_XSLS table, and then click Open Table.

  2. For each record in the table, do the following:

    1. Determine the file name for the record by comparing the fields with the previous query you did on the Published database. For example, if the NOTIFICATION_EMAIL_TYPE_UID = ab66e16a-be87-41c4-9811-0e9196437102, the NOTIFICATION_EMAIL_LCID = 1033, and the NOTIFICATION_EMAIL_MODE_ID = 0, the file name for that record is Reminder-StatusReport_Text_1033.xslt.

    2. In Visual Studio, create a new XSLT transformation file, delete the entire xsl:stylesheet element (including the xsl:template child element), and then save the file with the name you determined in the previous step.

    3. In the opened table in SQL Server Management Studio, right-click the NOTIFICATION_EMAIL_XSL field, and then click Copy.

    4. Paste the data into the XSLT transformation file in Visual Studio, and then save it.

Creating the OnSending Event Handler

The OnSending override method in the MyNotificationsEventReceiver class calls the static SendMyNotification method in the EventHandlerCommon class. In Procedure 2, you first create the override method, and then create the SendMyNotification method. Procedure 3 adds the GetResourceEmail method that SendMyNotification uses.

The OnSending event arguments expose the subject line and body of the notification. You can use data in the NotificationsPreSendEventArgs.xmlBody property to help design the OnSending event handler. If you create an event handler that does nothing, you can install the event handler, set a breakpoint, and then debug the event handler to examine e.xmlBody (see Step 3 in Procedure 2). For example, the debugger shows the following e.xmlBody data in a test where a Queue job fails due to a bad Web site when trying to publish a project and create a workspace.

<NotificationData notificationTypeUid="f4cfe6f2-e423-415e-8fb4-4568c5f832c1" 
                  emailTypeUid="9d538582-6257-4ff9-ab36-d4f3853aaea5" 
                  typeName="QueueJobFailedNotificationData2" 
                  recipientUid="88936e51-dcb3-4953-b191-ce1bfeb92049" 
                  lcid="1033" emailMode="0">
   <jobUid value="b2b31ac2-a3fe-4a99-b43f-9e860af6c42e" />
   <jobType value="CreateWssSite" />
   <jobState value="FailedNotBlocking" />
   <createdDate value="07/29/2007 16:14:08" />
   <jobUID value="b2b31ac2-a3fe-4a99-b43f-9e860af6c42e" />
   <percentComplete value="0" />
   <errorXmlString value="&amp;#60?xml version=&quot;1.0&quot; 
      encoding=&quot;utf-16&quot;?&amp;#62&#xD;&#xA;&amp;#60errinfo&amp;#62&#xD;&#xA;  
      &amp;#60general&amp;#62&#xD;&#xA;    
      &amp;#60class name=&quot;Site hierarchy does not exist.&quot;&amp;#62&#xD;&#xA;      
      &amp;#60error id=&quot;16407&quot; name=&quot;WSSWebHierarchyDoesNotExist&quot; 
      uid=&quot;413e3b1c-ae85-4db7-8297-dbe614a9fbc6&quot; &amp;#47&amp;#62&#xD;&#xA;    
      &amp;#60&amp;#47class&amp;#62&#xD;&#xA;    
      &amp;#60class name=&quot;Queue&quot;&amp;#62&#xD;&#xA;      
      &amp;#60error id=&quot;26000&quot; name=&quot;GeneralQueueJobFailed&quot; 
      uid=&quot;43c89c43-5819-4dc4-8396-47e23426accb&quot; 
      JobUID=&quot;b2b31ac2-a3fe-4a99-b43f-9e860af6c42e&quot; 
      ComputerName=&quot;ServerName&quot; GroupType=&quot;CreateWssSite&quot; 
      MessageType=&quot;CreateWssSiteMessage&quot; 
      MessageId=&quot;1&quot; Stage=&quot;&quot; &amp;#47&amp;#62&#xD;&#xA;    
      &amp;#60&amp;#47class&amp;#62&#xD;&#xA;  
      &amp;#60&amp;#47general&amp;#62&#xD;&#xA;&amp;#60&amp;#47errinfo&amp;#62" />
   <errorXml>
      <errinfo>
         <general>
            <class name="Site hierarchy does not exist.">
               <error id="16407" name="WSSWebHierarchyDoesNotExist" 
                      uid="413e3b1c-ae85-4db7-8297-dbe614a9fbc6" />
            </class>
            <class name="Queue">
               <error id="26000" name="GeneralQueueJobFailed" 
                      uid="43c89c43-5819-4dc4-8396-47e23426accb" 
                      JobUID="b2b31ac2-a3fe-4a99-b43f-9e860af6c42e" 
                      ComputerName="ServerName" GroupType="CreateWssSite" 
                      MessageType="CreateWssSiteMessage" MessageId="1" Stage="" />
            </class>
         </general>
      </errinfo>
   </errorXml>
</NotificationData>

The SendMyNotification method (Step 4 in Procedure 2) extracts relevant information such as the notification type and recipient GUID. To install the event handler, see Installing the OnSending Event Handler and Configuring SMTP in this article.

For the full code in the MyNotificationsEventReceiver and EventHandlerCommon classes, see Example in this article.

Procedure 2: To create the OnSending event handler:

  1. In Visual Studio, create a Windows class library. Name the project NotificationsEventHandler.

  2. Set references to the Microsoft.Office.Project.Server.Events.dll and Microsoft.Office.Project.Server.Library.dll assemblies.

  3. In Solution Explorer, rename Class1.cs MyNotificationsEventReceiver.cs. The MyNotificationsEventReceiver class inherits from the Microsoft.Office.Project.Server.Events.NotificationsEventReceiver class. Following is the complete code for the MyNotificationsEventReceiver class.

    using System;
    using System.Text;
    using PSEvents = Microsoft.Office.Project.Server.Events;
    using PSLibrary = Microsoft.Office.Project.Server.Library;
    
    namespace NotificationsEventHandler
    {
        public class MyNotificationsEventReceiver : PSEvents.NotificationsEventReceiver
        {
            public override void OnSending(
                PSLibrary.PSContextInfo contextInfo, PSEvents.NotificationsPreSendEventArgs e)
            {
                EventHandlerCommon.SendMyNotification("Notifications", "Sending", contextInfo, e);
            }
        }
    }
    
  4. Create the EventHandlerCommon class. The SendMyNotification method does most of the work of the event handler. Set the class constants and variables used for your Project Server installation and for sending e-mail messages.

    • PROJECT_SERVER_URI: Change the ServerName and name of the Project Web Access instance.

    • fromLine: Typically, the e-mail alias of the Project Server administrator who sets up the SMTP service.

    • smtpServer: Typically, the server name where the SMTP service is installed on the Project Web Access computer.

    • NOTIFICATIONS_DIR: Directory on the Project Server computer where you save the modified XSLT transformation files.

    using System;
    using System.Net;
    using System.Text;
    using System.IO;
    using System.Data;
    using System.Xml;
    using System.Xml.Xsl;
    using System.Net.Mail;
    using System.Diagnostics;
    using System.Reflection;
    using PSEvents = Microsoft.Office.Project.Server.Events;
    using PSLibrary = Microsoft.Office.Project.Server.Library;
    
    namespace NotificationsEventHandler
    {
        class EventHandlerCommon
        {
            private const int SMTP_PORT = 25;
            private const String EMAILCHARSET = "utf-8";
            private const string PROJECT_SERVER_URI = "http://ServerName/ProjectServerName/";
            private const string RESOURCE_WEBSERVICE = "_vti_bin/psi/resource.asmx";
            private const string NOTIFICATIONS_DIR = @"C:\Project\CustomNotifications\";
    
            private static String fromLine = "UserAlias@domain.com";
            private static String smtpServer = "SMTP_ServerName";
    
            public static void SendMyNotification(String eventSourceName, String eventName, 
                PSLibrary.PSContextInfo contextInfo, System.EventArgs args)
            {
                // Add code for SendMyNotification here . . .
            }
        }
    }
    
  5. In the SendMyNotification method, get the XML notification data and the Notifications subject line from the OnSending event arguments, and then create variables for sending e-mail messages.

    PropertyInfo[] properties = 
        args.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
    Object[] indexedProperties = null;
    
    string notificationDataXmlBody = properties[0].GetValue(args, indexedProperties).ToString();
    string messageSubjectLine = "Notifications Sending pre-event sample: "
        + properties[1].GetValue(args, indexedProperties).ToString();
    
    fromLine, and smtpServer.
    string toLine = string.Empty;
    string emailBody = string.Empty;
    bool isBodyHtml = false;
    bool useCustomXslt = false;
    
  6. Query the XML notification data for the following information (letters correspond to comments in the code):

    1. E-mail type GUID.

      // Load the XML.
      XmlDocument notificationDataXmlDocument = new XmlDocument();
      notificationDataXmlDocument.LoadXml(notificationDataXmlBody);
      
      XmlNode root = notificationDataXmlDocument.DocumentElement;
      
      Guid emailTypeUid = Guid.Empty;
      XmlAttribute xmlAttribute = root.Attributes["emailTypeUid"];
      emailTypeUid = new Guid(xmlAttribute.Value);
      
    2. Get the notification type GUID.

      Note

      You can use either the e-mail type or the notification type to determine which XSLT to use, by comparing the GUID with the Microsoft.Office.Project.Server.Notification field values. For example, compare the e-mail type with AlertEmailUid* or ReminderEmailUid*. Or, compare the notification type with AlertUid* or ReminderUid*.

      Guid notificationTypeUid = Guid.Empty;
      xmlAttribute = root.Attributes["notificationTypeUid"];
      notificationTypeUid = new Guid(xmlAttribute.Value);
      
    3. Get the recipient GUID.

      Guid recipientUid = Guid.Empty;
      xmlAttribute = root.Attributes["recipientUid"];
      recipientUid = new Guid(xmlAttribute.Value);
      
    4. Get the recipient e-mail address (and the recipient name, if you want it) by using the PSI Resource.ReadResources method. The GetResourceEmail method in Procedure 3 does this.

      toLine = GetResourceEmail(recipientUid);
      
    5. E-mail mode (0 for text and 1 for HTML).

      xmlAttribute = root.Attributes["emailMode"];
      isBodyHtml = Convert.ToInt32(xmlAttribute.Value) == 1;
      
    6. E-mail language (LCID, or locale ID).

      int lcid = 1033;
      xmlAttribute = root.Attributes["lcid"];
      lcid = Convert.ToInt32(xmlAttribute.Value);
      
    7. Use the e-mail type or the notification type to check which messages use a custom XSLT transformation file, and then use the custom XSLT to transform the message into the e-mail body.

      using (StringWriter strWriter = new StringWriter())
      {
          XslCompiledTransform xslt = new XslCompiledTransform();
      
          // Load the correct XSLT transformation for the e-mail type.
          XsltSettings xsltSettings = new XsltSettings(false, true);
          XmlUrlResolver xmlUrlResolver = new XmlUrlResolver();
          string xsltFilePath;
      
          // Here we use the notificationTypeUid instead of the emailTypeUid.
          // Ensure you compare with the correct field -- for example, 
          // Notification.AlertUidQueueJobFailed instead of Notification.AlertEmailUidQueueJobFailed.
          if (notificationTypeUid.Equals(PSLibrary.Notification.AlertUidQueueJobFailed))
          {
                  xsltFilePath =
                      string.Format(NOTIFICATIONS_DIR + "Alert-QueueJobFailed_{0}_{1}.xslt",
                      isBodyHtml ? "Html" : "Text", lcid.ToString());
                  xslt.Load(xsltFilePath, xsltSettings, xmlUrlResolver);
                  useCustomXslt = true;
          }
          else if (notificationTypeUid.Equals(PSLibrary.Notification.AlertUidResourceSubmitsStatusReport))
          {
                  xsltFilePath =
                      string.Format(NOTIFICATIONS_DIR + "Alert-ResourceSubmitsStatusReport_{0}_{1}.xslt",
                      isBodyHtml ? "Html" : "Text", lcid.ToString());
                  xslt.Load(xsltFilePath, xsltSettings, xmlUrlResolver);
                  useCustomXslt = true;
          }
          // Add cases for any other alert types needed here for custom notifications.  
      
          if (useCustomXslt)
          {
              XsltArgumentList xsltArgs = null;
      
              xslt.Transform(notificationDataXmlDocument, xsltArgs, strWriter);
              emailBody = strWriter.ToString();
          }
      }
      
  7. Send the custom e-mail message to the recipient.

    if (useCustomXslt)
    {
        using (MailMessage notifMailMessage = new MailMessage(fromLine, toLine))
        {
            Encoding encoding = Encoding.GetEncoding(EMAILCHARSET);
    
            notifMailMessage.Body = emailBody;
            notifMailMessage.Subject = messageSubjectLine;
            notifMailMessage.IsBodyHtml = isBodyHtml;
            notifMailMessage.BodyEncoding = encoding;
            notifMailMessage.SubjectEncoding = encoding;
    
            SmtpClient smtpClient = new SmtpClient(smtpServer, SMTP_PORT);
            smtpClient.Send(notifMailMessage);
        }
    }
    
  8. If you send a custom e-mail message, cancel the OnSending event so that Project Server does not send the default message.

    if (useCustomXslt)
    {
        // Cancel the default notification.
        ((PSEvents.IPreEventArgs)args).Cancel = true;
        ((PSEvents.IPreEventArgs)args).CancelReason =
            "Catching pre-event worked, changed the XSLT for notification.";
    }
    

Add the GetResourceEmail method, which uses an XML filter and the ReadResources method in the PSI. You could use the ReadResource method instead, but that also returns data in the ResourceDataSet object that you do not need.

Procedure 3: To add the GetResourceEmail method:

  1. Add a Web reference to http://ServerName/ProjectServerName/_vti_bin/psi/Resource.asmx, and name the Web reference ResourceWS.

  2. Add the following code to the EventHandlerCommon class. The code includes retrieval of the resourceName variable, which is not used in the sample application.

    private static string GetResourceEmail(Guid resourceUid)
    {
        string resourceName = String.Empty;
        string resourceEmail = String.Empty;
    
        ResourceWS.Resource resource = new ResourceWS.Resource();
        resource.Url = PROJECT_SERVER_URI + RESOURCE_WEBSERVICE;
        resource.Credentials = CredentialCache.DefaultCredentials;
    
        ResourceWS.ResourceDataSet resourceDS = new ResourceWS.ResourceDataSet();
        string tableName = resourceDS.Resources.TableName;
        string resUidColumn = resourceDS.Resources.RES_UIDColumn.ColumnName;
        string resNameColumn = resourceDS.Resources.RES_NAMEColumn.ColumnName;
        string emailColumn = resourceDS.Resources.WRES_EMAILColumn.ColumnName;
    
        PSLibrary.Filter.FieldOperationType equal = 
            PSLibrary.Filter.FieldOperationType.Equal;
    
        PSLibrary.Filter resDataFilter = new PSLibrary.Filter();
        resDataFilter.FilterTableName = tableName;
    
        resDataFilter.Fields.Add(
            new PSLibrary.Filter.Field(resUidColumn));
        resDataFilter.Fields.Add(
            new PSLibrary.Filter.Field(resNameColumn));
        resDataFilter.Fields.Add(
            new PSLibrary.Filter.Field(emailColumn));
    
        resDataFilter.Criteria = new PSLibrary.Filter.FieldOperator(
            equal, resUidColumn, resourceUid);
    
        string xmlFilter = resDataFilter.GetXml();
        bool autoCheckOut = false;
    
        resourceDS = resource.ReadResources(xmlFilter, autoCheckOut);
    
        if (null != resourceDS)
        {
            ResourceWS.ResourceDataSet.ResourcesRow resourceRow =
                (ResourceWS.ResourceDataSet.ResourcesRow)resourceDS.Resources.Rows[0];
    
            resourceName = resourceRow.RES_NAME;
            resourceEmail = resourceRow.WRES_EMAIL;
        }
        return resourceEmail;
    }
    

Installing the OnSending Event Handler and Configuring SMTP

The compiled event handler assembly is NotificationsEventHandler.dll. After you register the assembly in the global assembly cache, use Project Web Access to register the event handler for the Notifications Sending event. For example, the full assembly name in the Event Handler registration page in Project Web Access is NotificationsEventHandler,Version=1.0.0.0,Culture=Neutral,PublicKeyToken=d5a5e2ecd0a652a7. The full class name is NotificationsEventHandler.MyNotificationsEventReceiver. For detailed information about installing and debugging event handlers, see How to: Write and Debug a Project Server Event Handler.

Before you test the event handler, make sure the SMTP service on the test Project Server computer is working properly. For detailed information about installing and configuring the SMTP service, search for SMTP server configuration on Microsoft TechNet (http://technet.microsoft.com). For example, see SMTP Server Setup (IIS 6.0), HOW TO: Configure the SMTP Virtual Server for Message Delivery, and How to Configure a Windows Server 2003 Server as a Relay Server or Smart Host.

Caution noteCaution

Before you configure the SMTP service on a production Project Server installation, be sure to consult with your organization's network administrator.

General Steps to Configure an SMTP Virtual Server for Testing: Procedure 4 provides one way to configure an SMTP virtual server on a test installation of Project Server. The server does not authenticate mail and Project Server submits mail to the relay server in an anonymous manner. The relay server then submits mail in an authenticated manner to the SMTPHOST server (such as a server running Microsoft Exchange Server) in your network.

Procedure 4: To configure an SMTP virtual server for testing:

  1. In the Windows Components Wizard of the Control Panel Add or Remove Programs, go the Application Server details page, and then go to the Internet Information Services (IIS) details page. Select SMTP Service, and then install the service.

  2. Open the IIS Manager, right-click Default SMTP Virtual Server, and then click Properties.

  3. On the Access tab, click Authentication. Ensure that only Annonymous access is selected.

  4. Click Connection on the Access tab. In the Connection dialog box, click Only the list below, and then add the IP addresses that must have access to the SMTP virtual server. That is, add the IP address of the local Project Server computer and of any other computers you want to be able to use the SMTP virtual server.

  5. Click Relay on the Access tab, click All except the list below, and then select Allow all computers which successfully authenticate to relay, regardless of the list above. The computer list can be empty, or you can add restrictions if necessary.

  6. Click the Delivery tab, and then click Outbound Security. Click Integrated Windows Authentication, and then type the credentials of the user account (not a service account) that will submit the e-mail messages to the SMTPHOST server. Typically, that is the administrator of the test installation of Project Server.

  7. Click Advanced on the Delivery tab. In the Advanced Delivery dialog box, type the fully qualified domain name of the local Project Server computer. For example, type ServerName.subdomain.domain.com. Click Check DNS to determine whether the name is valid.

  8. In the Smart host text box, type the fully qualified domain name of the server running Exchange Server in your network that is configured to act as a smart host e-mail server. For example, type SMTPHOST.subdomain.domain.com.

  9. Restart IIS.

  10. On the Start menu, click Run, and then type services.msc. In the Services dialog box, restart the Simple Mail Transport Protocol service.

  11. Open the SharePoint 3.0 Central Administration site, click the Operations tab, and then click Outgoing e-mail settings. Type the fully qualified domain name of the SMTP virtual server in the Outbound SMTP server textbox. For example, type ServerName.subdomain.domain.com. In the From address and Reply-to address text boxes, type the Project Server administrator e-mail alias, and then click OK.

E-mail sent to the SMTP virtual server is routed to the C:\Inetpub\mailroot\Queue directory. If the credentials in the Outbound Security part of the Default SMTP Virtual Server Properties dialog box are correct, and the account has the necessary Send-As permissions, mail appears in the Queue directory and then almost immediately is submitted to SMTPHOST. If mail appears in the Queue directory and is not sent, try restarting the Project Server computer. If the mail still remains in the Queue directory, enable logging on the General tab of the Default SMTP Virtual Server Properties dialog box. Check all of the properties on the Advanced tab of the Logging Properties dialog box. By default, the logs are saved in the [Windows]\System32\LogFiles directory.

You can use telnet to send a test message to the SMTP virtual server. Following is a telnet conversation in a Command Prompt window. Commands typed by the user are in bold text.

telnet 10.198.117.141 25
220 SERVERNAME.subdomain.domain.com CompanyName ESMTP MAIL Service, Version:
 6.0.3790.3959 ready at  Tue, 31 Jul 2007 12:47:53 -0700
helo
250 SERVERNAME.subdomain.domain.com Hello [10.198.117.141]
mail from:user@domain.com
250 2.1.0 user@domain.com....Sender OK
rcpt to:user@domain.com
250 2.1.5 user@domain.com
data
354 Start mail input; end with <CRLF>.<CRLF>
subject: This is a testThis is a test, it is only a test. Had it been areal e-mail, you would have gotten some real information..
250 2.6.0 <SERVERNAMET8lmnAEDx0000000a@SERVERNAME.subdomain.domain.com> Queued mail for delivery
quit
221 2.0.0 SERVERNAME.subdomain.domain.com Service closing transmission channel
Connection to host lost.

The line that contains Queued mail for delivery shows that the SMTP virtual server successfully sent the mail to the Queue directory.

Testing the OnSending Event Handler

Check the Events page in Project Web Access (http://ServerName/ProjectServerName/_layouts/pwa/Admin/Events.aspx) to ensure the event handler is correctly registered for the Notifications Sending event. The example SendMyNotification method checks for only two notification types, AlertUidQueueJobFailed and AlertUidResourceSubmitsStatusReport. You can easily create a failed queue job to test the event handler, using the sample LoginDemo application in the Project 2007 SDK download.

Procedure 5: To test the OnSending event handler:

  1. In the directory you specified for the custom XSLT files (for example, C:\Project\CustomNotifications), use Visual Studio to modify both the Alert-QueueJobFailed_Html_1033.xslt and Alert-QueueJobFailed_Text_1033.xslt files. For example, modify the first line of the e-mail Body section as follows:

    <xsl:variable name="Body">CUSTOM NOTIFICATION: Your {%2} job failed.
        {%0}{%0}Its current state is {%3}.{%0}{%0}It was {%4}% complete.
        {%0}{%0}It entered the queue at {%5}.</xsl:variable>
    
  2. In Project Web Access, click Personal Settings in Quick Launch, and then click Manage My Alerts and Reminders.

  3. On the Manage My Alerts and Reminders page, select Alert me immediately when: Any of my queue jobs fails. You can also choose the language and HTML or Text in the Language Setting section. Click Save.

  4. Start the LoginDemo application, log on your test Project Server, and then type a project name to create, for example, type Test Notification. Clear Default Project Workspace URL, and then type a bad workspace site name. For example, type badsubsite/TestNotification. Click Create Project.

The application creates the Test Notification project in the Draft database, but the call to QueuePublish fails because the subsite does not exist. (The LoginDemo application reports the incorrect project workspace because it does not check the queue after the call to QueuePublish.) The queue job fails, and you get an e-mail that includes the following information.

[Subject:]  
Notifications Sending pre-event sample: 
Your queue job CreateWssSite failed. 
Please contact your administrator for assistance

[Body:] 
CUSTOM NOTIFICATION: Your CreateWssSite job failed.  
Its current state is FailedNotBlocking.  It was 0% complete.  
It entered the queue at 07/29/2007 16:14:08.

To get more information about the job failure, please go 
to Project Web Access.  Select Personal Settings from the 
left menu.  Then select My Queued Jobs.

The errors returned from the queue are as follows:
 Error ID: 16407
 Error ID: 26000

Because the SendMyNotification method does use the custom XSLT file and sends an e-mail message, it cancels the OnSending pre-event. When the event is canceled, Project Server does not use the default transform data in the MSP_NOTIFICATION_XSLS table or send the default message.

Open the Event Viewer on the Project Server computer. Table 1 lists the Application events related to the notification, with the most recent event at the top.

Table 1. Application events from the OnSending event handler actions

Source

Category

Event Description

Office SharePoint Server

Project Server Server-side Events

Standard Information:PSI Entry Point:

Project User: DOMAIN\username

Correlation ID: 91b4c678-df9b-4e50-8371-77b6a1486a15

PWA Site URL: http://ServerName/PWA

SSP Name: SharedServices1

PSError: GeneralActionCanceledByEventHandler (22000)

Action canceled by event handler. Reason: Catching pre-event worked, changed the XSLT for notification.

Office SharePoint Server

Project Server Server-side Events

Operation canceled by event handler: assembly: NotificationsEventHandler, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d5a5e2ecd0a652a7 class: NotificationsEventHandler.MyNotificationsEventReceiver event: NotificationsSending handler: Microsoft.Office.Project.Server.Events.NotificationsSendingHandler, method: OnSending

Office SharePoint Server

Project Server Queue

Standard Information:PSI Entry Point:

Project User: DOMAIN\username

Correlation ID: 91b4c678-df9b-4e50-8371-77b6a1486a15

PWA Site URL: http:// ServerName/PWA

SSP Name: SharedServices1

PSError: GeneralQueueJobFailed (26000)

A queue job has failed. This is a general error logged by the Project Server Queue every time a job fails - . . .

Office SharePoint Server

Project Server SharePoint Integration

Standard Information:PSI Entry Point:

Project User: DOMAIN\username

Correlation ID: 91b4c678-df9b-4e50-8371-77b6a1486a15

PWA Site URL: http://ServerName/PWA

SSP Name: SharedServices1

PSError: WSSWebHierarchyDoesNotExist (16407)

You cannot create a project workspace at (null) because that path does not exist.

The most recent event in Table 1 shows the CancelReason value set in the SendMyNotification method. If the event handler fails, the Event Viewer shows the reason for the failure. For example, the event in Table 2 results when the class constants and variables are not set to the correct values, so the event handler does not have access to Project Server or the SMTP virtual server.

Table 2. Application event when the event handler throws an exception

Source

Category

Event Description

Office SharePoint Server

Project Server Server-Side Events

Event Handler for event \NotificationsSending\ of type \NotificationsEventHandler.MyNotificationsEventReceiver\ threw an exception: The parameter to cannot be an empty string.

Parameter name: to

at System.Net.Mail.MailMessage..ctor(String from, String to)

at NotificationsEventHandler.EventHandlerCommon.SendMyNotification(String eventSourceName, String eventName, PSContextInfo contextInfo, EventArgs args)

at NotificationsEventHandler.MyNotificationsEventReceiver.OnSending(PSContextInfo contextInfo, NotificationsPreSendEventArgs e)

Example

The following code includes the content of the MyNotificationsEventReceiver.cs and EventHandlerCommon.cs files that are provided in the Project 2007 SDK download.

/* Code for MyNotificationsEventReceiver.cs */
using System;
using System.Text;
using PSEvents = Microsoft.Office.Project.Server.Events;
using PSLibrary = Microsoft.Office.Project.Server.Library;

namespace NotificationsEventHandler
{
    public class MyNotificationsEventReceiver : PSEvents.NotificationsEventReceiver
    {
        public override void OnSending(
            PSLibrary.PSContextInfo contextInfo, PSEvents.NotificationsPreSendEventArgs e)
        {
            EventHandlerCommon.SendMyNotification("Notifications", "Sending", contextInfo, e);
        }
    }
}

/* Code for EventHandlerCommon.cs */
using System;
using System.Net;
using System.Text;
using System.IO;
using System.Data;
using System.Xml;
using System.Xml.Xsl;
using System.Net.Mail;
using System.Diagnostics;
using System.Reflection;
using PSEvents = Microsoft.Office.Project.Server.Events;
using PSLibrary = Microsoft.Office.Project.Server.Library;

namespace NotificationsEventHandler
{
    class EventHandlerCommon
    {
        private const int SMTP_PORT = 25;
        private const String EMAILCHARSET = "utf-8";
        private const string PROJECT_SERVER_URI = "http://ServerName/ProjectServerName/";
        private const string RESOURCE_WEBSERVICE = "_vti_bin/psi/resource.asmx";

        private static String fromLine = "UserAlias@domain.com";
        private static String smtpServer = "SMTP_ServerName";


        public static void SendMyNotification(String eventSourceName, String eventName, 
            PSLibrary.PSContextInfo contextInfo, System.EventArgs args)
        {
            // Get the handler's input parameters: Notifications XML and Notifications Subject Line
            PropertyInfo[] properties = 
                args.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
            Object[] indexedProperties = null;

            const string NOTIFICATIONS_DIR = "C:\\Project\\CustomNotifications\\";

            string notificationDataXmlBody = properties[0].GetValue(args, indexedProperties).ToString();
            string messageSubjectLine = "Notifications Sending pre-event sample: "
                + properties[1].GetValue(args, indexedProperties).ToString();

            // Set parameters for sending e-mail messages. 
            // Class constants and variables include SMTP_PORT, EMAILCHARSET, fromLine, and smtpServer.
            string toLine = string.Empty;
            string emailBody = string.Empty;
            bool isBodyHtml = false;
            bool useCustomXslt = false;

            try
            {
                // Load the XML.
                XmlDocument notificationDataXmlDocument = new XmlDocument();
                notificationDataXmlDocument.LoadXml(notificationDataXmlBody);

                XmlNode root = notificationDataXmlDocument.DocumentElement;

                // Query the XML notification document for information.
                // (a) Determine the e-mail type. 
                //     You can compare the e-mail type with AlertEmailUid* or ReminderEmailUid*  
                //     field values in Microsoft.Office.Project.Server.Notification.
                Guid emailTypeUid = Guid.Empty;
                XmlAttribute xmlAttribute = root.Attributes["emailTypeUid"];
                emailTypeUid = new Guid(xmlAttribute.Value);

                // (b) Determine the notification type.
                //     You can compare the notification type with AlertUid* or ReminderUid*  
                //     field values in Microsoft.Office.Project.Server.Notification.
                Guid notificationTypeUid = Guid.Empty;
                xmlAttribute = root.Attributes["notificationTypeUid"];
                notificationTypeUid = new Guid(xmlAttribute.Value);

                // (c) Determine the recipient.
                Guid recipientUid = Guid.Empty;
                xmlAttribute = root.Attributes["recipientUid"];
                recipientUid = new Guid(xmlAttribute.Value);

                // (d) Get the recipient's e-mail address.
                toLine = GetResourceEmail(recipientUid);

                // (e) Determine the e-mail mode: text or HTML.
                xmlAttribute = root.Attributes["emailMode"];
                isBodyHtml = Convert.ToInt32(xmlAttribute.Value) == 1;

                // (f) Determine the e-mail language.
                int lcid = 1033;
                xmlAttribute = root.Attributes["lcid"];
                lcid = Convert.ToInt32(xmlAttribute.Value);

                // (g) Determine the e-mail body.
                using (StringWriter strWriter = new StringWriter())
                {
                    XslCompiledTransform xslt = new XslCompiledTransform();

                    // Load the correct XSLT for the e-mail type.
                    XsltSettings xsltSettings = new XsltSettings(false, true);
                    XmlUrlResolver xmlUrlResolver = new XmlUrlResolver();
                    string xsltFilePath;

                    // Here we are using the notificationTypeUid instead of the emailTypeUid.
                    // Make sure you compare with the correct field -- for example, 
                    // Notification.AlertUidQueueJobFailed instead of Notification.AlertEmailUidQueueJobFailed.
                    if (notificationTypeUid.Equals(PSLibrary.Notification.AlertUidQueueJobFailed))
                    {
                            xsltFilePath =
                                string.Format(NOTIFICATIONS_DIR + "Alert-QueueJobFailed_{0}_{1}.xslt",
                                isBodyHtml ? "Html" : "Text", lcid.ToString());
                            xslt.Load(xsltFilePath, xsltSettings, xmlUrlResolver);
                            useCustomXslt = true;
                    }
                    else if (notificationTypeUid.Equals(PSLibrary.Notification.AlertUidResourceSubmitsStatusReport))
                    {
                            xsltFilePath =
                                string.Format(NOTIFICATIONS_DIR + "Alert-ResourceSubmitsStatusReport_{0}_{1}.xslt",
                                isBodyHtml ? "Html" : "Text", lcid.ToString());
                            xslt.Load(xsltFilePath, xsltSettings, xmlUrlResolver);
                            useCustomXslt = true;
                    }
                    // Add cases for any other alert types needed here for custom notifications,  
                    // from Microsoft.Office.Project.Server.Library.Notification library.

                    if (useCustomXslt)
                    {
                        XsltArgumentList xsltArgs = null;

                        xslt.Transform(notificationDataXmlDocument, xsltArgs, strWriter);
                        emailBody = strWriter.ToString();
                    }
                }

                if (useCustomXslt)
                {
                    using (MailMessage notifMailMessage = new MailMessage(fromLine, toLine))
                    {
                        Encoding encoding = Encoding.GetEncoding(EMAILCHARSET);

                        notifMailMessage.Body = emailBody;
                        notifMailMessage.Subject = messageSubjectLine;
                        notifMailMessage.IsBodyHtml = isBodyHtml;
                        notifMailMessage.BodyEncoding = encoding;
                        notifMailMessage.SubjectEncoding = encoding;

                        SmtpClient smtpClient = new SmtpClient(smtpServer, SMTP_PORT);
                        smtpClient.Send(notifMailMessage);
                    }
                }
            }
            catch (Exception ex)
            {
                using (MailMessage notifMailMessage = new MailMessage(fromLine, toLine))
                {
                    Encoding encoding = Encoding.GetEncoding(EMAILCHARSET);

                    notifMailMessage.Body = string.Format(
                        "Notifications Sending PreEvent failed. Exception:\n{0}", ex);
                    notifMailMessage.Subject = "Notifications Sending PreEvent failed.";
                    notifMailMessage.IsBodyHtml = isBodyHtml;
                    notifMailMessage.BodyEncoding = encoding;
                    notifMailMessage.SubjectEncoding = encoding;

                    SmtpClient smtpClient = new SmtpClient(smtpServer, SMTP_PORT);
                    smtpClient.Send(notifMailMessage);
                }
                // Throw the exception again, to get an Application event.
                throw new Exception(string.Format(
                    "Notifications Sending PreEvent failed. Exception:\n{0}", ex));
            }

            // If there is no custom XSLT file to use, no custom e-mail message is sent, so 
            // do not cancel the notification event.
            if (useCustomXslt)
            {
                // Cancel the default notification.
                ((PSEvents.IPreEventArgs)args).Cancel = true;
                ((PSEvents.IPreEventArgs)args).CancelReason =
                    "Catching pre-event worked, changed the XSLT file for notification.";
            }
        }

        // Use an XML filter with the ReadResources method to return a ResourceDataSet with 
        // only the information needed. Return the WRES_EMAIL field for the specified resource GUID.
        // Because it is not used, you could remove the resourceName variable and references.
        private static string GetResourceEmail(Guid resourceUid)
        {
            string resourceName = String.Empty;
            string resourceEmail = String.Empty;

            ResourceWS.Resource resource = new ResourceWS.Resource();
            resource.Url = PROJECT_SERVER_URI + RESOURCE_WEBSERVICE;
            resource.Credentials = CredentialCache.DefaultCredentials;

            ResourceWS.ResourceDataSet resourceDS = new ResourceWS.ResourceDataSet();
            string tableName = resourceDS.Resources.TableName;
            string resUidColumn = resourceDS.Resources.RES_UIDColumn.ColumnName;
            string resNameColumn = resourceDS.Resources.RES_NAMEColumn.ColumnName;
            string emailColumn = resourceDS.Resources.WRES_EMAILColumn.ColumnName;

            PSLibrary.Filter.FieldOperationType equal = 
                PSLibrary.Filter.FieldOperationType.Equal;

            PSLibrary.Filter resDataFilter = new PSLibrary.Filter();
            resDataFilter.FilterTableName = tableName;

            resDataFilter.Fields.Add(
                new PSLibrary.Filter.Field(resUidColumn));
            resDataFilter.Fields.Add(
                new PSLibrary.Filter.Field(resNameColumn));
            resDataFilter.Fields.Add(
                new PSLibrary.Filter.Field(emailColumn));

            resDataFilter.Criteria = new PSLibrary.Filter.FieldOperator(
                equal, resUidColumn, resourceUid);

            string xmlFilter = resDataFilter.GetXml();
            bool autoCheckOut = false;

            resourceDS = resource.ReadResources(xmlFilter, autoCheckOut);

            if (null != resourceDS)
            {
                ResourceWS.ResourceDataSet.ResourcesRow resourceRow =
                    (ResourceWS.ResourceDataSet.ResourcesRow)resourceDS.Resources.Rows[0];

                resourceName = resourceRow.RES_NAME;
                resourceEmail = resourceRow.WRES_EMAIL;
            }
            return resourceEmail;
        }
    }
}

The complete code sample and Visual Studio solution files are provided in the Project 2007 SDK download.

Compiling the Code

The sample code in the download uses hard-coded values for Project Server and e-mail parameters. Before you compile the code, change the class constants and variables to match the values for your instance of Project Web Access, Project Server installation, and SMTP virtual server.

To test a recompiled event handler, you must delete the previous version from the global assembly cache, unregister the event handler in Project Web Access, add the recompiled version to the global assembly cache, and register it again in Project Web Access.

See Also

Tasks

How to: Write and Debug a Project Server Event Handler

Concepts

Project Server Events

Other Resources

SMTP Server Setup (IIS 6.0)

HOW TO: Configure the SMTP Virtual Server for Message Delivery

How to Configure a Windows Server 2003 Server as a Relay Server or Smart Host