I recently ran into an issue where one of my customer was unable to send mail using System.Net.Mail(.Net 2.0), we always got the "Authentication failed" error.
We tried sending mail using Microsoft CDO for Windows 2000 Library(Cdosys) and System.Web.Mail and the mails went just fine. if Cdosys works System.Web.Mail(SWM) would normally work because SWM is essentially a wrapper over Cdosys. How can you send mails using Cdosys? Click here
Why is one API able to send mail and the other fails? We enabled Network Tracing in our .net code and below is what the logs show:
System.Net.Sockets Verbose: 0 : [7240] 00000000 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : 220 mail.XYZYZYZ
System.Net.Sockets Verbose: 0 : [7240] 00000010 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : XYZXYZZ.com ESMT
System.Net.Sockets Verbose: 0 : [7240] 00000020 : 00 00 00 00 00 00 00 00 00 00 00 00 00 : P Gday mate..
System.Net.Sockets Verbose: 0 : [7240] Exiting Socket#32176063::Receive() -> 45#45
System.Net.Sockets Verbose: 0 : [7240] Socket#32176063::Send()
System.Net.Sockets Verbose: 0 : [7240] Data from Socket#32176063::Send
System.Net.Sockets Verbose: 0 : [7240] 00000000 : 45 48 4C 4F 20 41 4B 41-53 48 42 0D 0A : EHLO AKASHB..
System.Net.Sockets Verbose: 0 : [7240] Exiting Socket#32176063::Send() -> 13#13
System.Net.Sockets Verbose: 0 : [7240] Socket#32176063::Receive()
System.Net.Sockets Verbose: 0 : [7240] Data from Socket#32176063::Receive
System.Net.Sockets Verbose: 0 : [7240] 00000000 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : 250-mail.XYZYZYZ
System.Net.Sockets Verbose: 0 : [7240] 00000010 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : XYZXYZZ.com..250
System.Net.Sockets Verbose: 0 : [7240] 00000020 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : -PIPELINING..250
System.Net.Sockets Verbose: 0 : [7240] 00000030 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : -SIZE 20971520..
System.Net.Sockets Verbose: 0 : [7240] 00000040 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : 250-VRFY..250-ET
System.Net.Sockets Verbose: 0 : [7240] 00000050 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : RN..250-AUTH PLA
System.Net.Sockets Verbose: 0 : [7240] 00000060 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : IN LOGIN..250-EN
System.Net.Sockets Verbose: 0 : [7240] 00000070 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : HANCEDSTATUSCODE
System.Net.Sockets Verbose: 0 : [7240] 00000080 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : S..250-8BITMIME.
System.Net.Sockets Verbose: 0 : [7240] 00000090 : 00 00 00 00 00 00 00 00-00 00 : .250 DSN..
System.Net.Sockets Verbose: 0 : [7240] Exiting Socket#32176063::Receive() -> 154#154
System.Net Verbose: 0 : [7240] SmtpLoginAuthenticationModule#61150033::Authenticate()
System.Net Verbose: 0 : [7240] Exiting SmtpLoginAuthenticationModule#61150033::Authenticate()
System.Net.Sockets Verbose: 0 : [7240] Socket#32176063::Send()
System.Net.Sockets Verbose: 0 : [7240] Data from Socket#32176063::Send
System.Net.Sockets Verbose: 0 : [7240] 00000000 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : AUTH login Y2222
System.Net.Sockets Verbose: 0 : [7240] 00000010 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : Wwwwwwwwwwwwwwww
System.Net.Sockets Verbose: 0 : [7240] 00000020 : 00 00 00 00 00 00 00 00 00 00 00 00 00 : mmmmmmmmmmm..
System.Net.Sockets Verbose: 0 : [7240] Exiting Socket#32176063::Send() -> 45#45
System.Net.Sockets Verbose: 0 : [7240] Socket#32176063::Receive()
System.Net.Sockets Verbose: 0 : [7240] Data from Socket#32176063::Receive
System.Net.Sockets Verbose: 0 : [7240] 00000000 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : 334 VXNlcm5hbWU6
System.Net.Sockets Verbose: 0 : [7240] 00000010 : 0D 0A : ..
System.Net.Sockets Verbose: 0 : [7240] Exiting Socket#32176063::Receive() -> 18#18
System.Net Verbose: 0 : [7240] SmtpLoginAuthenticationModule#61150033::Authenticate()
System.Net Verbose: 0 : [7240] Exiting SmtpLoginAuthenticationModule#61150033::Authenticate()
System.Net.Sockets Verbose: 0 : [7240] Socket#32176063::Send()
System.Net.Sockets Verbose: 0 : [7240] Data from Socket#32176063::Send
System.Net.Sockets Verbose: 0 : [7240] 00000000 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : xxxxxxxxxxxxxxxx=
System.Net.Sockets Verbose: 0 : [7240] 00000010 : 0D 0A : ..
System.Net.Sockets Verbose: 0 : [7240] Exiting Socket#32176063::Send() -> 18#18
System.Net.Sockets Verbose: 0 : [7240] Socket#32176063::Receive()
System.Net.Sockets Verbose: 0 : [7240] Data from Socket#32176063::Receive
System.Net.Sockets Verbose: 0 : [7240] 00000000 : 33 33 34 20 55 47 46 7A-63 33 64 76 63 6D 51 36 : 334 UGFzc3dvcmQ6
System.Net.Sockets Verbose: 0 : [7240] 00000010 : 0D 0A : ..
System.Net.Sockets Verbose: 0 : [7240] Exiting Socket#32176063::Receive() -> 18#18
System.Net Verbose: 0 : [7240] SmtpLoginAuthenticationModule#61150033::Authenticate()
System.Net Verbose: 0 : [7240] Exiting SmtpLoginAuthenticationModule#61150033::Authenticate()
System.Net Error: 0 : [7240] Exception in the SmtpClient#3888474::Send - Authentication failed.
Note:Data in the logs has been altered to hide confidential information
To request LOGIN authentication, the client issues the AUTH command with the parameter LOGIN and the user name to be used for authentication, base64-encoded as specified in [RFC4648]. For example, if the client's user name was "Charlie", then the client would initiate AUTH LOGIN as follows (AUTH_LOGIN_COMMAND_USER):
AUTH LOGIN Q2hhcmxpZQ==<CR><LF>
If AUTH LOGIN is not supported, then the server should responds with a 504 error Message as specified in [RFC4954] section 4. If AUTH LOGIN is supported on the server, then the server responds with the AUTH_LOGIN_Password_Challenge:
334 UGFzc3dvcmQ6<CR><LF>
The client then responds with the password to be used for authentication, base64-encoded as specified in [RFC4648]. For example, if the client's password was "password", then client would respond with the following Login_Password_Response:
cGFzc3dvcmQ=<CR><LF>
If the authentication is successful, then the server issues a LOGIN_Succeeded_Response or a LOGIN_Failed_Response, corresponding to a 235 reply for success or a 535 reply for a failure [RFC4954].
In this case, even though the user name is passed with the AUTH LOGIN the server responds with the AUTH_LOGIN_Username_Challenge again:
334 VXNlcm5hbWU6<CR><LF>
Now since the client has already sent the user name, it instead of sending the user name, sends the password. The server then responds with the AUTH_LOGIN_Password_Challenge:
334 UGFzc3dvcmQ6<CR><LF>
The client then sends nothing and the Authentication fails! We can clearly see that the SMTP server is not respecting the user name sent with the AUTH LOGIN command. It is optional for the client to send the user name([initial-response]) with the AUTH LOGIN command, if sent the SMTP server should respect it. More details about the SMTP Service Extension for Authentication can be found in the [RFC4954].
Why did Cdosys and SWM work? Both the API’s do not send the user name along with the AUTH LOGIN and therefore everything works fine.
Is there a way we can change this behaviour of System.Net.Mail i.e. not send the user name along with the AUTH LOGIN? No.
What are the alternatives?
1)Use CDOSYS (Does not send the User Name with the AUTH Login)
2)Use System.Web.Mail (Does not send the User Name with the AUTH Login)
3)Contact the SMTP Server owner and have them fix the server.
Enjoy!
In continuation to my previous post, the customer also had a SMTP Agent that was installed on a Edge Server and did context sensitive email analysis for incoming SMTP messages. In this case only 20 email could be processed by the SMTP Agent.
How were the SMTP messages being sent? The customer had a multithreaded application to send out mail. Fortunately, I was able to reproduce this too.In my case when I sent more than 20 mails using a multithreaded application, I ran into the following exception:
System.Runtime.InteropServices.COMException (0x80040211): The message could not be sent to the SMTP server. The transport error code was 0x800ccc67. The server
response was 421 4.3.2 The maximum number of concurrent connections has exceeded a limit, closing transmission channel at CDO.MessageClass.Send()
at SendSMTPMails.MassMailer.SendMail() in C:\SendSMTPMails\SendSMTPMails\Program.cs:line 63 Exception caught.
This is a restrictions on the Receive Connector in Exchange 2007. You will need to configure the receive connector to allow MaxInboundConnectionPerSource to a desired number. The default value is 20.
You can set the value by running the Set-ReceiveConnector cmdlet.
Cmdlet:Set-ReceiveConnector "Receive connector Name" -MaxInboundConnectionPerSource 100
Enjoy!
I recently ran in an issue where one of my customer was developing a context sensitive email analysis software for MS Exchange 2007. The results of the analysis was used to decide how that email would be dealt with.
Due to the complexity of the analysis, the estimated processing time per email was about 15 to 30 seconds. To realize an acceptable throughput of emails per minute, they needed to ensure that the email processing was strongly parallelized. Therefore they encapsulated the analysis itself into a separate web service which was being called from a Routing Agent.
Using the asynchronous programming pattern(http://msdn.microsoft.com/en-us/library/cc720860.aspx), recommended by Microsoft for long I/O waiting times,they encountered a limitation in parallel processing. They noticed that at one time they could only process 6 email at a time. The processing of the seventh email would begin only when one of the mails submitted earlier was done.
In case of the Hub role this was double throughput compared to the synchronous programming pattern but still nowhere near the required rate of parallel processing. At the existing rate they would only be able to process a maximum of 24 emails per minute. The requirement was to process a minimum of 100 emails per minute.
Fortunately I was able to reproduce the issue in my lab. Below is the code I used to reproduce the behaviour:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Xml;
using System.Reflection;
using Microsoft.Exchange.Data.Mime;
using Microsoft.Exchange.Data.Transport;
using Microsoft.Exchange.Data.Transport.Email;
using Microsoft.Exchange.Data.Transport.Routing;
namespace AsyncRoutingAgent
{
/// <summary>
/// Agent factory
/// </summary>
public class MyRoutingAgentFactory :RoutingAgentFactory
{
/// <summary>
/// Create a new RoutingAgent.
/// </summary>
/// <param name="server">Exchange Hub server</param>
/// <returns>a new agent</returns>
public override RoutingAgent CreateAgent(SmtpServer server)
{
return new MyRoutingAgent();
}
}
public class MyRoutingAgent : RoutingAgent
{
private object fileLock = new object();
public MyRoutingAgent()
{
base.OnSubmittedMessage += new SubmittedMessageEventHandler(MyRoutingAgent_OnSubmittedMessage);
}
void MyRoutingAgent_OnSubmittedMessage(SubmittedMessageEventSource source, QueuedMessageEventArgs e)
{
WriteLog("Entering OnSubmittedMessage");
// Create an instance of the Web Service via the proxy class
WriteLog("Creating Instance of Web Service");
AsyncRoutingAgent.localhost.StockService objWebService = new AsyncRoutingAgent.localhost.StockService();
// Get the async context so that the server knows that this event should be handled asynchronously.
// Also save the event source and event args so that they can be accessed from the callback.
WriteLog("Calling GetAgentAsyncContext");
AsyncState<SubmittedMessageEventSource, QueuedMessageEventArgs> asyncState =
new AsyncState<SubmittedMessageEventSource, QueuedMessageEventArgs>(
source,
e,
this.GetAgentAsyncContext());
WriteLog("Hooking the Event Handler");
objWebService.GetStockQuoteCompleted += new AsyncRoutingAgent.localhost.GetStockQuoteCompletedEventHandler(objWebService_GetStockQuoteCompleted);
WriteLog("Calling the Service");
objWebService.GetStockQuoteAsync("MSFT", 15, asyncState);
WriteLog("Just before Returning");
return;
}
void objWebService_GetStockQuoteCompleted(object sender, AsyncRoutingAgent.localhost.GetStockQuoteCompletedEventArgs e)
{
AsyncState<SubmittedMessageEventSource, QueuedMessageEventArgs> asyncState =
(AsyncState<SubmittedMessageEventSource, QueuedMessageEventArgs>)e.UserState;
WriteLog(e.Result);
// Handle the results of the asynchronous I/O here.
// The event source and event args are available from the asyncState.
// Tell the server that this event is complete.
asyncState.Complete();
WriteLog("Call Completed");
}
public void WriteLog(String Comment)
{
lock (fileLock)
{
try
{
string logDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + @"\Log";
string logFile = logDir + @"\log.txt";
if (!Directory.Exists(logDir))
{
Directory.CreateDirectory(logDir);
}
if (!File.Exists(logFile))
{
File.CreateText(logFile).Close();
}
using (StreamWriter logWriter = File.AppendText(logFile))
{
logWriter.Write(DateTime.Now.ToString()+ ":" + Comment+ Environment.NewLine);
logWriter.Flush();
}
}
catch (System.IO.IOException ex)
{
Debug.WriteLine(ex.ToString());
}
}
}
/// <summary>
/// This class wraps all the state that has to be saved
/// for an asynchronous event.
/// </summary>
private class AsyncState<SourceType, ArgsType>
{
private SourceType source;
private ArgsType args;
private AgentAsyncContext asyncContext;
public AsyncState(
SourceType source,
ArgsType args,
AgentAsyncContext asyncContext)
{
this.source = source;
this.args = args;
this.asyncContext = asyncContext;
}
public SourceType Source
{
get { return this.source; }
}
public ArgsType Args
{
get { return this.args; }
}
public void Complete()
{
this.asyncContext.Complete();
this.source = default(SourceType);
this.args = default(ArgsType);
this.asyncContext = null;
}
}
}
}
The above code generates a log which clearly shows the problem.
09/04/2009 1:41:25 AM:Entering OnSubmittedMessage
09/04/2009 1:41:26 AM:Entering OnSubmittedMessage
09/04/2009 1:41:27 AM:Entering OnSubmittedMessage
09/04/2009 1:41:28 AM:Entering OnSubmittedMessage
09/04/2009 1:41:28 AM:Entering OnSubmittedMessage
09/04/2009 1:41:30 AM:Entering OnSubmittedMessage
09/04/2009 1:41:40 AM:Call Completed
09/04/2009 1:41:40 AM:Entering OnSubmittedMessage
09/04/2009 1:41:42 AM:Call Completed
09/04/2009 1:41:42 AM:Entering OnSubmittedMessage
09/04/2009 1:41:42 AM:Call Completed
…
In the logs above you can see that "Entering OnSubmittedMessage" is fired 6 times between 1:41:25 Am - 1:41:30 and the next call to "Entering OnSubmittedMessage" is made at 1:41:40 AM only after one of the call from the Web Service returned.The web service has a delay of 15 seconds to simulated a time consuming service.
Now to the solution. The Exchange Transport creates multiple instances of the agent if it has multiple messages to process, and the number of instances is limited by the following settings:
MaxExecutingJobs: Range 1 to 50
MaxJobThreads: Range 1-25 (Default value 3)
These setting have to be defined in the edgetransport.exe.config under the appSettings section. In case when the agent is run asynchronously we will only need to alter the value of MaxExecutingJobs and increase its value.
Add the following lines to the edgetransport.exe.config under the appSettings section. You will need to restart the Microsoft Transport and the EdgeTransport.exe
<add key="MaxExecutingJobs" value="50"/>
<add key="MaxJobThreads" value="3"/>
In case you need more that 50 jobs to be executed at the same time, you will need to run the code on a machine that has more than 1 processors, since the actual MaxExecutingJobs value is calculated by multiplying it with the number of processors your machine has.
Enjoy!
Note:In a Routing Agent we should not be doing stuff that takes a long time to execute. We did advise the customer to take a alternative approach to the entire solution.
Do you want to make “ReplyAll” the last button in the Ribbon while reading mail(Inspector) in Outlook 2007?
Yes, it can be done. All you have to do is make a simple COM Add-in that implements IRibbonExtensibility or a VSTO Add-in and provide it with the appropriate Ribbon XML.
You cannot hide/remove the buttons in the built in groups using the “visible” attribute but you can definitely hide the entire group. This is exactly what i did!
The group where the “ReplyAll” button is placed is named “GroupRespond”. I altered its visibility and then created two new groups. The first group had just the “Reply” and “Forward” buttons and was placed before the “GroupActions”, the second group had the “ReplyAll” button and by default got added to the end of the Ribbon.
Below is the Ribbon XML that I used:
<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" onLoad="Ribbon_OnLoad">
<ribbon>
<tabs>
<tab idMso="TabReadMessage">
<group idMso="GroupRespond" visible="false">
</group>
<group id="Respond" label="Respond" insertBeforeMso="GroupActions">
<button idMso="Reply" size="large" />
<button idMso="Forward" size="large" />
</group>
<group id="ReplyAllOnly" label="Respond">
<button idMso="ReplyAll" size="large" />
</group>
</tab>
</tabs>
</ribbon>
</customUI>
Hope this helps!
I ran into this same issue and then discovered the following KB:
Error message when you visit a Web site that is hosted on IIS 7.0: "HTTP Error 404.11 – URL_DOUBLE_ESCAPED"
http://support.microsoft.com/kb/942076
Setting the allowDoubleEscaping="true" did resolve my problem but this approach slightly compromises the server security by leaving it open to “Double Encoding” attacks.
The cause of the problem was the “+” sign in the name of some of the .eml files. In fact any file that has a “+” in the file name + is hosted on IIS7 + does not have the allowDoubleEscaping set to true will give you the HTTP Error 404.11.
IIS7 rejecting URLs containing +
http://blogs.iis.net/thomad/archive/2007/12/17/iis7-rejecting-urls-containing.aspx
Have you ever tried sending a mail to someone and it end up with someone else! Beware there could be a Transport Agent that's doing this:-).
I ran into an issue where I needed to rewrite the address the mail was being sent to. Not going into too much as to why somebody would want to do this, I wrote a Transport Agent that would do the needful.
The below sample C# code written in Visual Studio 2005 changes the To address of the mail that is being sent out. There is no conditional logic as of now and the code assumes that there is only one recipient.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using Microsoft.Exchange.Data.Mime;
using Microsoft.Exchange.Data.Transport;
using Microsoft.Exchange.Data.Transport.Email;
using Microsoft.Exchange.Data.Transport.Routing;
namespace Samples.Agents.MyRoutingAgent
{
public class MyRoutingAgentFactory :RoutingAgentFactory
{
public override RoutingAgent CreateAgent(SmtpServer server)
{
return new MyRoutingAgent();
}
}
public class MyRoutingAgent : RoutingAgent
{
private object fileLock = new object();
public MyRoutingAgent()
{
base.OnSubmittedMessage += new SubmittedMessageEventHandler(MyRoutingAgent_OnSubmittedMessage);
}
void MyRoutingAgent_OnSubmittedMessage(SubmittedMessageEventSource source, QueuedMessageEventArgs e)
{
lock (fileLock)
{
try
{
string logDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + @"\Log";
string logFile = logDir + @"\log.txt";
if (!Directory.Exists(logDir))
{
Directory.CreateDirectory(logDir);
}
if (!File.Exists(logFile))
{
File.CreateText(logFile).Close();
}
using (StreamWriter logWriter = File.AppendText(logFile))
{
logWriter.Write(Environment.NewLine + "-------------------------------------------------------------------------------" + Environment.NewLine);
//Alter the P1 headers.The P1 address is used for routing.
for(int intCounter=e.MailItem.Recipients.Count-1;intCounter>=0; intCounter--)
{
logWriter.WriteLine("Original Recipient:" + e.MailItem.Recipients[intCounter].OriginalRecipient);
if (e.MailItem.Recipients[intCounter].Address.IsValid)
{
e.MailItem.Recipients.Remove(e.MailItem.Recipients[intCounter]);
if (e.MailItem.Recipients.CanAdd)
{
e.MailItem.Recipients.Add(new RoutingAddress("administrator@mycompany.com"));
}
}
}
//Alter the P2 headers so that the mail displays the correct recipient display Name
EmailRecipientCollection erToRecipientCollection;
erToRecipientCollection = e.MailItem.Message.To;
logWriter.Write(Environment.NewLine + "-------------------------------------------------------------------------------" + Environment.NewLine);
foreach (EmailRecipient rec in erToRecipientCollection)
{
logWriter.WriteLine("Original Display Name:" + rec.DisplayName);
logWriter.WriteLine("Original SMTP address:" + rec.SmtpAddress);
rec.DisplayName = "Administrator";
rec.SmtpAddress = "administrator@mycompany.com";
logWriter.WriteLine("Changed to <Administrator>administrator@mycompany.com");
}
}
logWriter.Write(Environment.NewLine + "-------------------------------------------------------------------------------" + Environment.NewLine);
logWriter.Flush();
}
}
catch (System.IO.IOException ex)
{
Debug.WriteLine(ex.ToString());
}
}
return;
}
}
}
It is IMPORTANT that you change both the P1 and the P2 headers for this to work correctly. To build the Agent successfully you will need to add references to Microsoft.Exchange.Data.Transport and the Microsoft.Exchange.Data.Common namespaces. These dll's can be found in the Program Files\Microsoft\Exchange Server\Public folder.
To Install/Uninstall the Agent on the Hub Server I have created script files that do the job for me(makes life easier).
Install.PS1
#Copy the agent Dll to the C:\MyAgent folder.
$EXDIR="C:\MyAgent"
Net Stop MSExchangeTransport
Write-Output "Registering agent"
Install-TransportAgent -Name "My Routing Agent Sample" -AssemblyPath $EXDIR\RoutingAgent.dll -TransportAgentFactory Samples.Agents.MyRoutingAgent.MyRoutingAgentFactory
Write-Output "Enabling agent"
Enable-TransportAgent -Identity "My Routing Agent Sample"
Get-TransportAgent -Identity "My Routing Agent Sample"
Net Start MSExchangeTransport
Write-Output "Install Complete. Please exit the Exchange Management Shell."
UnInstall.PS1
$EXDIR="C:\MyAgent"
Net Stop MSExchangeTransport
Write-Output "Disabling Agent..."
Disable-TransportAgent -Identity "My Routing Agent Sample" -Confirm:$false
Write-Output "Uninstalling Agent.."
Uninstall-TransportAgent -Identity "My Routing Agent Sample" -Confirm:$false
Net Start MsExchangeTransport
Write-Output "Uninstall Complete."
Need more information on Transport Agents?
Transport Agents
http://msdn.microsoft.com/en-us/library/aa579185.aspx
Enjoy!
The easiest solution is to create a COM component and register that component with component services (COM+) running the component under a specific user identity. Why do I say this? Read on...
In my case the Web site/Application was configured to run under the DefaultAppPool (Identity = Network Service) and you I wanted to use PowerShell to Enable a Mailbox on Exchange 2007. The code is running on the Exchange 2007 Server itself. What are the available options?
1) Impersonate the user who has the required permissions to Enable a Mailbox in code.
2) Create a new Application Pool, configure it's Identity to a user who has the required permissions to Enable a Mailbox.
3) Create a COM component and register that component with component services(COM+) running the component under a user who has the required permissions to Enable a Mailbox.
Lets discuss each of these options:
1) Impersonate the user who has the required permissions to Enable a Mailbox in code
This is how the ASP.NET code looks like. Thanks to Dan for the code.
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Text;
using System.Security;
using System.Security.Principal;
using System.Runtime.InteropServices;
public partial class _Default : System.Web.UI.Page
{
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool LogonUser(string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);
// Holds the new impersonation token.
private IntPtr _userToken = IntPtr.Zero;
private WindowsImpersonationContext _impersonationContext = null;
private void EnableMailbox(string domain, string domainController)
{
RunspaceConfiguration config = RunspaceConfiguration.Create();
PSSnapInException warning;
// Load Exchange PowerShell snap-in.
config.AddPSSnapIn("Microsoft.Exchange.Management.PowerShell.Admin", out warning);
if (warning != null) throw warning;
// This is where be begin the Impersonation
BeginImpersonation(domain);
using (Runspace thisRunspace = RunspaceFactory.CreateRunspace(config))
{
try
{
thisRunspace.Open();
using (Pipeline thisPipeline = thisRunspace.CreatePipeline())
{
//Please change parameter values.
thisPipeline.Commands.Add("Enable-Mailbox");
thisPipeline.Commands[0].Parameters.Add("Identity", @"DOM157711\xxxxxxx");
thisPipeline.Commands[0].Parameters.Add("Database", @"XXXXXXX-8\First Storage Group\Mailbox Database");
// Need the line below when impersonating. KB943937. Need rollup 1 for sp1.
thisPipeline.Commands[0].Parameters.Add("DomainController", domainController);
try
{
thisPipeline.Invoke();
}
catch (Exception exx)
{
Response.Write("Error: " + exx.ToString());
}
// Check for errors in the pipeline and throw an exception if necessary.
if (thisPipeline.Error != null && thisPipeline.Error.Count > 0)
{
StringBuilder pipelineError = new StringBuilder();
pipelineError.AppendFormat("Error calling Enable-Mailbox.");
foreach (object item in thisPipeline.Error.ReadToEnd())
{
pipelineError.AppendFormat("{0}\n", item.ToString());
}
throw new Exception(pipelineError.ToString());
}
}
}
finally
{
thisRunspace.Close();
EndImpersonation();
}
}
}
private void BeginImpersonation(string domain)
{
//Please change the User Name and Password
string UserName = "UserName";
string Password = "Password";
EndImpersonation();
Response.Write("User Before Impersonation: " + WindowsIdentity.GetCurrent().Name + "</BR>");
bool success = LogonUser(
UserName,
domain,
Password,
2,
0,
ref _userToken);
// Did it work?
if (!success) throw new Exception(string.Format("LogonUser returned error {0}", System.Runtime.InteropServices.Marshal.GetLastWin32Error()));
WindowsIdentity fakeId = new WindowsIdentity(_userToken);
_impersonationContext = fakeId.Impersonate();
Response.Write("User After Impersonation: " + WindowsIdentity.GetCurrent().Name + "</BR>");
}
private void EndImpersonation()
{
if (_impersonationContext != null)
{
try
{
_impersonationContext.Undo();
}
finally
{
_impersonationContext.Dispose();
_impersonationContext = null;
}
}
}
protected void Button1_Click(object sender, EventArgs e)
{
// Use the current domain and domain controller. The domain controller parameter is needed for Impersonation to work
// Please change ualues to suit you needs.
string domain = "DOM157711";
string domainController = "XXXXX-8";
try
{
EnableMailbox(domain, domainController);
}
catch (Exception ex)
{
Response.Write(ex.ToString());
}
finally
{
Response.Write("Done..");
}
}
}
This is how my Web.Config looks like:
<?xml version="1.0"?>
<configuration>
<appSettings/>
<connectionStrings/>
<system.web>
<compilation debug="false">
<assemblies>
<add assembly="System.Management.Automation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
</assemblies>
</compilation>
<authentication mode="None"/>
</system.web>
</configuration>
This code works and does what I want! Now I decided to move this code off the Exchange 2007 server and deploy it on to a Windows 2003 box that has the Exchange 2007 Management tools(SP1 and RU 5) Installed. When I did this it no longer worked! I get an exception instead:
Error: Microsoft.Exchange.Configuration.Tasks.ThrowTerminatingErrorException: Database "XXXXXXX-8\First Storage Group\Mailbox Database" was not found. Please make sure you have typed it correctly. ---> Microsoft.Exchange.Configuration.Tasks.ManagementObjectNotFoundException: Database "XXXXXXX-8\First Storage Group\Mailbox Database" was not found. Please make sure you have typed it correctly. at Microsoft.Exchange.Configuration.Tasks.DataAccessTask`1.GetDataObject[TObject](IIdentityParameter id, IConfigDataProvider session, ObjectId rootID, Nullable`1 notFoundError, LocalizedString multipleFoundError) at Microsoft.Exchange.Management.RecipientTasks.EnableMailbox.InternalBeginProcessing() at Microsoft.Exchange.Configuration.Tasks.Task.BeginProcessing() --- End of inner exception stack trace --- at Microsoft.Exchange.Configuration.Tasks.Task.ThrowTerminatingError(Exception exception, ErrorCategory category, Object target) at Microsoft.Exchange.Configuration.Tasks.Task.ProcessUnhandledException(Exception e) at Microsoft.Exchange.Configuration.Tasks.Task.BeginProcessing() at System.Management.Automation.Cmdlet.DoBeginProcessing() at System.Management.Automation.CommandProcessorBase.DoBegin()
When I alter the same code to run in a Windows Application, it works. This for sure has something to do with Impersonation and IIS being involved. Not going into details of why I got the error I decided to look for alternatives as I was running short on time.
2) Create a new Application Pool, configure it's Identity to a user who has the required permissions to Enable a Mailbox
The simplest way I could get my code to work from the Windows 2003 box was to create a new Application Pool in IIS and configure its Identity to a user who had permissions to Enable a mailbox. Now this leaves me with a very very big security hole, since my entire application is running under an account which is quite powerful, I can only imagine the risks.
If you can guarantee that security is taken care of, you could use this approach but I personally would never use it. having said that, what else could I do? I do not want to run my code on the Exchange 2007 Server.
3) Create a COM component and register that component with component services(COM+) running the component under a user who has the required permissions to Enable a Mailbox.
This is the only approach that worked for me in this case and also in the past. Here is how you can get started:
Creating the C# Component
Build and configure a COM+ component to host the PowerShell code and ensure that it is running with the credentials of a service account and not inheriting any credentials from the caller.
Creating the project…
1)Start Visual Studio 2005
2)Select File->New->Project…
3)Under C#, select Windows
4)In the “Template Types” select “Class Library”
5)In the Name box type “PowerShellComponent”
6)Click OK.
7)Add references to System.EnterpriseServices, System.Management.Automation(browse to the C:\Program Files\Reference Assemblies\Microsoft\WindowsPowerShell\v1.0\).
Signing the Assembly...
1)Right click on the PowerShellComponent project in the Solution Explorer and click properties. In the signing tab check the box to “Sign the assembly” and create a new strong name key called “PowerShellComponent.snk”.
2)Still in the project properties window, select the Application tab and click “Assembly Information…”, check the box that says, “Make assembly COM-Visible”.
3)Open the AssemblyInfo.cs and add “using System.EnterpriseServices;” to the top and the following lines at the bottom of the file…
[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationName("PowerShellComponent")]
[assembly: Description("Simple PowerShell Component Sample")]
[assembly: ApplicationAccessControl(
false,
AccessChecksLevel = AccessChecksLevelOption.Application,
Authentication = AuthenticationOption.None,
ImpersonationLevel = ImpersonationLevelOption.Identify)]
Add the ManagementCommands class…
1)Select the “Solution Explorer” view tab.
Rename the class1.cs file to “ManagementCommands.cs”.
2)Remove the code generated by the Wizard.
Cut and paste the code provided below:
using System;
using System.Collections.Generic;
using System.Text;
using System.Management.Automation.Runspaces;
using System.EnterpriseServices;
using System.Security;
using System.Security.Principal;
using System.Runtime.InteropServices;
namespace PowerShellComponent
{
public class ManagementCommands : System.EnterpriseServices.ServicedComponent
{
public String EnableMailbox(string domain, string domainController)
{
String ErrorText="";
RunspaceConfiguration config = RunspaceConfiguration.Create();
PSSnapInException warning;
// Load Exchange PowerShell snap-in.
config.AddPSSnapIn("Microsoft.Exchange.Management.PowerShell.Admin", out warning);
if (warning != null) throw warning;
using (Runspace thisRunspace = RunspaceFactory.CreateRunspace(config))
{
try
{
thisRunspace.Open();
using (Pipeline thisPipeline = thisRunspace.CreatePipeline())
{
//Please change parameter values.
thisPipeline.Commands.Add("Enable-Mailbox");
thisPipeline.Commands[0].Parameters.Add("Identity", @"DOM157711\xxxxxxx");
thisPipeline.Commands[0].Parameters.Add("Database", @"XXXXXXX-8\First Storage Group\Mailbox Database");
// Need the line below when impersonating. KB943937. Need rollup 1 for sp1.
thisPipeline.Commands[0].Parameters.Add("DomainController", domainController);
try
{
thisPipeline.Invoke();
}
catch (Exception ex)
{
ErrorText = "Error: " + ex.ToString();
}
// Check for errors in the pipeline and throw an exception if necessary.
if (thisPipeline.Error != null && thisPipeline.Error.Count > 0)
{
StringBuilder pipelineError = new StringBuilder();
pipelineError.AppendFormat("Error calling Enable-Mailbox.");
foreach (object item in thisPipeline.Error.ReadToEnd())
{
pipelineError.AppendFormat("{0}\n", item.ToString());
}
ErrorText = ErrorText + "Error: " + pipelineError.ToString();
}
}
}
finally
{
thisRunspace.Close();
EndImpersonation();
}
}
if (ErrorText == "")
return "Success";
else
return ErrorText;
}
public string GetIdentity()
{
AppDomain.CurrentDomain.SetPrincipalPolicy(System.Security.Principal.PrincipalPolicy.WindowsPrincipal);
System.Security.Principal.WindowsPrincipal user = System.Threading.Thread.CurrentPrincipal as System.Security.Principal.WindowsPrincipal;
return user.Identity.Name;
}
}
}
Create and Configure the COM+ Component
1. Build the PowerShellComponent project in Visual Studio
2. From the Visual Studio command prompt run “regsvcs PowerShellComponent.dll”
3. Open “Component Services” from “Administrative Tools”
4. Under “Component Services”, “Computers”, “My Computer”, “COM+ Applications”, find “PowerShellComponent” and right click on it then select “Properties”
5. On the identity tab:
a. Configure “This user” as an account that has Exchange Admin privileges on your Exchange server.
6. Click OK
Code for the Web Application(ASP.Net) to Consume PowerShellComponent
Below is the code that needs to be pasted in the Code Behind file(This code assumes we are using Default.aspx.cs)
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Text;
using System.Security;
using System.Security.Principal;
using System.Runtime.InteropServices;
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
// Use the current domain and domain controller
// Please change ualues to suit you needs.
string domain = "DOM157711";
string domainController = "XXXXXXX-8";
try
{
EnableMailbox(domain, domainController);
}
catch (Exception ex)
{
Response.Write(ex.ToString());
}
finally
{
Response.Write("Done..");
}
}
private void EnableMailbox(string domain, string domainController)
{
Response.Write(string.Format("<b>Web Application Context:</b> {0}<br>", WindowsIdentity.GetCurrent().Name));
// Create the COM+ component
PowerShellComponent.ManagementCommands objManage = new PowerShellComponent.ManagementCommands();
String Results;
Results = objManage.EnableMailbox(domain1, domainController1);
Response.Write(Results);
// Print out the security context of the component
Response.Write(string.Format("<br><b>Component Context:</b> {0}<br><br>", objManage.GetIdentity()));
objManage = null;
}
}
Copy PowerShellComponent.dll from the output directory of the PowerShellComponent project to the “bin” under you Web Application and build the Web Project and test.
Talking a look at the results
When you access this page from another machine as different user or anonymous user you will notice that the web application and the COM+ component are running in different security contexts. For example in my test, I access the page as anonymous user and when I hit the web I got the following result…
Web Application Context: NT AUTHORITY\NETWORK SERVICE
Success
Component Context: DOM157711\akashb
The key here is that Web Application is running under NT AUTHORITY\NETWORK SERVICE and has no permission to Enable a Mailbox. The web form makes the call to our COM+ component which always executes the PowerShellComponent code in the context of account that we configured in COM+, no matter who the calling process is running as.
Using the COM+ approach has always worked for me, it also help you avoid other issues as outlined by Matt in his post.
Enjoy!
I recently came across an issue where I needed to get the details about Exchange User from within an Outlook Add-in. Below is a screen shot of the details I am referring to.
In Outlook 2007 its very simple, just use the ExchangeUser Object and you will get the details. More details can be found in the article below:
ExchangeUser Object Members
http://msdn.microsoft.com/en-us/library/bb176658.aspx
Below is sample C# code that shows how to use the ExchangeUser Object:
using System;
using System.Collections.Generic;
using System.Text;
using Outlook = Microsoft.Office.Interop.Outlook;
namespace GetUserDetails
{
class Program
{
static void Main(string[] args)
{
Outlook.Application oApp;
Outlook.NameSpace oNameSpace;
Outlook.SelectNamesDialog oSNDialog;
Outlook.AddressEntry oAEntry;
Outlook.ExchangeUser oEUser;
oApp = new Outlook.Application();
oNameSpace = oApp.GetNamespace("MAPI");
oNameSpace.Logon(null, null, true, false);
oSNDialog = oNameSpace.GetSelectNamesDialog();
oSNDialog.NumberOfRecipientSelectors = Outlook.OlRecipientSelectors.olShowTo;
oSNDialog.Caption = "Select User";
oSNDialog.ToLabel = "User Name";
oSNDialog.ShowOnlyInitialAddressList = true;
oSNDialog.AllowMultipleSelection = false;
oSNDialog.Display();
foreach(Outlook.Recipient oRecipient in oSNDialog.Recipients)
{
oAEntry = oRecipient.AddressEntry;
if(oAEntry.AddressEntryUserType == Outlook.OlAddressEntryUserType.olExchangeUserAddressEntry
|| oAEntry.AddressEntryUserType == Outlook.OlAddressEntryUserType.olExchangeRemoteUserAddressEntry)
{
oEUser = oAEntry.GetExchangeUser();
Console.WriteLine(oEUser.Name);
Console.WriteLine(oEUser.BusinessTelephoneNumber);
Console.WriteLine(oEUser.MobileTelephoneNumber);
Console.WriteLine(oEUser.Department);
oEUser = null;
}
oAEntry = null;
}
oNameSpace.Logoff();
oNameSpace = null;
}
}
}
However in Outlook 2003 the ExchangeUser Object is not there. The only way to do it is query the AD based on the AddressEntry.Address(LegacyexchangeDN). Below is sample C# code that uses the DirectoryServices namespace to query the AD based on LegacyexchangeDN.
using System;
using System.Collections.Generic;
using System.Text;
using System.DirectoryServices;
namespace GetUserDetailsBasedOnLegacyDN
{
class Program
{
static void Main(string[] args)
{
//This is the legacy exchange DN
string LEDN = "/O=ms/OU=east/cn=Recipients/cn=whatever";
DirectoryEntry objRootDSE = new DirectoryEntry("GC://RootDSE");
String strRoot = objRootDSE.Properties["rootDomainNamingContext"].Value.ToString();
DirectoryEntry objRoot = new DirectoryEntry("GC://" + strRoot);
objRoot.RefreshCache();
DirectorySearcher objSrch = new DirectorySearcher(objRoot);
objSrch.Filter = "(&(objectClass=user)(objectCategory=person)(legacyExchangeDN= " + LEDN + "))";
objSrch.SearchScope = SearchScope.Subtree;
DirectoryEntry objUser = new DirectoryEntry();
objUser = objSrch.FindOne().GetDirectoryEntry();
Console.WriteLine("Mobile:" + objUser.Properties["mobile"].Value);
Console.WriteLine("Number:" + objUser.Properties["telephoneNumber"].Value);
Console.WriteLine("Department:" + objUser.Properties["department"].Value);
objUser = null;
objSrch = null;
objRoot = null;
objRootDSE = null;
}
}
}
Note: I am not an AD expert, you might need to tweak the code depending on your environment and your needs.
Are you someone who maintains Exchange 2003 servers? Did you ever have a requirement to set the Mailbox quotas for a lot of users at one go? Here is your solution. The sample code below uses ADSI and Cdoexm to set mailbox quotas for multiple user.
The following sample is a simple VBScript code sample that sets the StoreQuota, OverQuotaLimit, Hardlimit for a mailbox. To use this sample, paste the following code in a new text file, and then name the file SetMailboxQuota.txt:
'This script sets the StoreQuota, OverQuotaLimit, Hardlimit for a mailbox.
' USAGE: cscript SetMailboxQuota.vbs DATAFILE
' This code has to be run on a box that has ESM or Exchange 2003 Installed.
Dim obArgs
Dim cArgs
Set obArgs = WScript.Arguments
cArgs = obArgs.Count
Main
Sub Main()
Dim FileSysObj
Dim DataFileName
Dim DataFile
Dim sAMAccoutname
Dim vDistinguishedName
Const ForReading = 1, ForWriting = 2, ForAppending = 8, TristateFalse = 0
On error Resume Next
If cArgs <> 1 Then
WScript.Echo "Usage: cscript SetMailboxQuota.vbs DATAFILE"
Exit Sub
End If
Set FileSysObj = CreateObject("Scripting.FileSystemObject")
DataFileName = obArgs.Item(0)
Set DataFile = FileSysObj.OpenTextFile(DataFileName, ForReading, False,0)
Do While Not DataFile.AtEndOfStream
sAMAccoutname = DataFile.ReadLine
vDistinguishedName = GetDNFromSam(sAMAccoutname)
If vDistinguishedName <> "" then
WScript.Echo "Sam Account Name:" & sAMAccoutname
WScript.Echo "Distinguished Name:" & vDistinguishedName
'Please change limits as per your needs
'The Size Specified is in Kb
Call SetLimits(vDistinguishedName, 18432,20480,25600)
If Err.Number <> 0 Then
sMsg = "Error Setting Limits"
sMsg = sMsg & Err.Number & " " & Err.Description
WScript.Echo sMsg
End If
else
WScript.Echo "Could not get Distinguished Name for " & sAMAccoutname
end if
WScript.Echo
Loop
DataFile.Close
Set DataFile = Nothing
Set FileSysObj = Nothing
End Sub
Function GetDNFromSam(strsAMAccountName)
Dim iAdCont
Dim iAdGC
Dim Conn
Dim Com
Dim Rs
Dim varGCAdsPath
Dim strQuery
' Get the Global Catalog
Set iAdCont = GetObject("GC:")
For Each iAdGC In iAdCont
varGCAdsPath = iAdGC.ADsPath
Next
Set Conn = createobject("ADODB.Connection")
Set Com = createobject("ADODB.Command")
' Open the Connection
Conn.Provider = "ADsDSOObject"
Conn.Open "ADs Provider"
' Build the query to find the user based on his Alias
strQuery = "<" & varGCAdsPath & ">;(sAMAccountName=" & strsAMAccountName & ");mailNickName,distinguishedName;subtree"
Com.ActiveConnection = Conn
Com.CommandText = strQuery
Set Rs = Com.Execute
GetDNFromSam = Rs.Fields("distinguishedName")
'Clean Up
Rs.Close
Conn.Close
Set Rs = Nothing
Set Com = Nothing
Set Conn = Nothing
Set iAdCont = Nothing
Set iAdGC = Nothing
End Function
Sub SetLimits(strUserDN, vStoreQuota,vOverQuotaLimit,vHardLimit)
Const AD_MODE_READ_WRITE = 3
Dim strLDAP
Dim objPerson
Dim objMailbox
strLDAP = "LDAP://" & strUserDN
Set objPerson = CreateObject("CDO.Person")
objPerson.DataSource.Open strLDAP,,AD_MODE_READ_WRITE
Set objMailbox = objPerson.GetInterface("IMailboxStore")
'check if the user is mailbox enabled
If objMailbox.HomeMDB = "" Then
Wscript.Echo "No Mailbox found."
Else
'Give Warning at
Wscript.Echo "Current StoreQuota:" & objMailbox.StoreQuota
Wscript.Echo "Setting StoreQuota to:" & vStoreQuota & "(KB)"
objMailbox.StoreQuota = vStoreQuota
'Prevent Sending at
Wscript.Echo "Current OverQuotaLimit:" & objMailbox.OverQuotaLimit & "(KB)"
Wscript.Echo "Setting OverQuotaLimit to:" & vOverQuotaLimit & "(KB)"
objMailbox.OverQuotaLimit = vOverQuotaLimit
'Prevent Sending and Recieving at
Wscript.Echo "Current Hardlimit:" & objMailbox.HardLimit & "(KB)"
Wscript.Echo "Setting StoreQuota to:" & vHardLimit & "(KB)"
objMailbox.HardLimit = vHardLimit
' Clearing defaults
Wscript.Echo "Disabling Database Defaults"
objMailbox.EnableStoreDefaults = false
objPerson.DataSource.Save
End If
Set objMailbox = Nothing
Set objPerson = Nothing
End Sub
The list of sAMAccountName's can be provided via a text file(Datafile). The Datafile contains the sAMAccountName's of the users(one on each line). So assuming your Datafile is called "sAMAccounts.txt" and is on the C:\, you would run the script as follows:
C:\>Cscript SetMailboxQuota.vbs C:\sAMAccounts.txt
The account that you are logged on to the computer must have permissions to change the limits on the mailboxes.
The following sample is a simple VBScript code sample that that uses CDOEX and ADO to iterate through multiple mailboxes and dumps out the contact data to a text file. This code must be run on the Exchange server.
To use this sample, paste the following code in a new text file, and then name the file DumpContacts.vbs:
'This script must be run on an Exchange Server.
'USAGE: cscript DumpContacts.vbs DOMAIN Datafile(FullPath)
Dim obArgs
Dim cArgs
Set obArgs = WScript.Arguments
cArgs = obArgs.Count
Main
Sub Main()
Dim FileSysObj
Dim DataFileName
Dim DataFile
Dim alias
Const ForReading = 1, ForWriting = 2, ForAppending = 8, TristateFalse = 0
If cArgs <> 2 Then
WScript.Echo "Usage: cscript DumpContacts.vbs DOMAIN Datafile(FullPath)"
Exit Sub
End If
Set FileSysObj = CreateObject("Scripting.FileSystemObject")
DataFileName = obArgs.Item(1)
Set DataFile = FileSysObj.OpenTextFile(DataFileName, ForReading, False,0)
'Read line by line to get the mailbox alias from the Datafile
Do While Not DataFile.AtEndOfStream
alias = DataFile.ReadLine
'Dump contact data to a csv file
Call DumpContacts(obArgs.Item(0), alias)
Loop
DataFile.Close
'Clean up
Set DataFile = Nothing
Set FileSysObj = Nothing
End Sub
Sub DumpContacts(Domain, Mailbox)
Dim Rs
Dim Rec
Dim strURL
Dim sMsg
Dim strLocalPath
Dim ContactData
On Error Resume Next
' Specify the URL to a folder or an item.
strLocalPath = "MBX/" & Mailbox &"/Contacts"
Set Rec = CreateObject("ADODB.Record")
Set Rs = CreateObject("ADODB.Recordset")
strURL = "file://./backofficestorage/" & Domain & "/" & strLocalPath
Rec.Open strURL
If Err.Number <> 0 Then
sMsg = "Error Connecting to the Contact folder for User " & Mailbox & " "
sMsg = sMsg & Err.Number & " " & Err.Description
WScript.Echo sMsg
Exit Sub
End If
Set Rs.ActiveConnection = Rec.ActiveConnection
Rs.Source = "SELECT ""DAV:href"", " & _
" ""urn:schemas:contacts:email1"", " & _
" ""urn:schemas:contacts:nickname"", " & _
" ""urn:schemas:contacts:title"" " & _
"FROM scope('shallow traversal of """ & strURL & """') "
Rs.Open
while Not Rs.EOF
Dim iPerson
Set iPerson = Createobject("CDO.Person")
iPerson.DataSource.Open Rs.Fields("DAV:href").Value, Rec.ActiveConnection
ContactData = iPerson.Company & ";"
ContactData = ContactData & iPerson.Email & ";"
ContactData = ContactData & iPerson.Email2 & ";"
ContactData = ContactData & iPerson.Email3 & ";"
ContactData = ContactData & iPerson.FileAs & ";"
ContactData = ContactData & iPerson.FileAsMapping & ";"
ContactData = ContactData & iPerson.FirstName & ";"
ContactData = ContactData & iPerson.HomeCity & ";"
ContactData = ContactData & iPerson.HomeCountry & ";"
ContactData = ContactData & iPerson.HomeFax & ";"
ContactData = ContactData & iPerson.HomePhone & ";"
ContactData = ContactData & Replace(iPerson.HomePostalAddress, vbcrlf," ") & ";"
ContactData = ContactData & iPerson.HomePostalCode & ";"
ContactData = ContactData & iPerson.HomePostOfficeBox & ";"
ContactData = ContactData & iPerson.HomeState & ";"
ContactData = ContactData & iPerson.HomeStreet & ";"
ContactData = ContactData & iPerson.Initials & ";"
ContactData = ContactData & iPerson.LastName & ";"
ContactData = ContactData & Replace(iPerson.MailingAddress, vbcrlf," ") & ";"
ContactData = ContactData & iPerson.MailingAddressID & ";"
ContactData = ContactData & iPerson.MiddleName & ";"
ContactData = ContactData & iPerson.MobilePhone & ";"
ContactData = ContactData & iPerson.NamePrefix & ";"
ContactData = ContactData & iPerson.NameSuffix & ";"
ContactData = ContactData & iPerson.Title & ";"
ContactData = ContactData & iPerson.WorkCity & ";"
ContactData = ContactData & iPerson.WorkCountry & ";"
ContactData = ContactData & iPerson.WorkFax & ";"
ContactData = ContactData & iPerson.WorkPager & ";"
ContactData = ContactData & iPerson.WorkPhone & ";"
ContactData = ContactData & Replace(iPerson.WorkPostalAddress, vbcrlf," ") & ";"
ContactData = ContactData & iPerson.WorkPostalCode & ";"
ContactData = ContactData & iPerson.WorkPostOfficeBox & ";"
ContactData = ContactData & iPerson.WorkState & ";"
ContactData = ContactData & iPerson.WorkStreet & ";"
ContactData = ContactData & iPerson.Fields("urn:schemas:contacts:weddinganniversary").Value & ";"
ContactData = ContactData & iPerson.Fields("urn:schemas:contacts:account").Value & ";"
ContactData = ContactData & iPerson.Fields("urn:schemas:contacts:bday").Value & ";"
ContactData = ContactData & iPerson.Fields("urn:schemas:contacts:billinginformation").Value & ";"
ContactData = ContactData & iPerson.Fields("urn:schemas:contacts:businesshomepage").Value & ";"
ContactData = ContactData & iPerson.Fields("urn:schemas:contacts:co").Value & ";"
ContactData = ContactData & iPerson.Fields("urn:schemas:contacts:cn").Value & ";"
ContactData = ContactData & iPerson.Fields("urn:schemas:contacts:computernetworkname").Value & ";"
ContactData = ContactData & iPerson.Fields("urn:schemas:contacts:dn").Value & ";"
ContactData = ContactData & iPerson.Fields("urn:schemas:contacts:gender").Value & ";"
ContactData = ContactData & iPerson.Fields("urn:schemas:contacts:weddinganniversary").Value & ";"
ContactData = ContactData & iPerson.Fields("urn:schemas:contacts:manager").Value & ";"
ContactData = ContactData & iPerson.Fields("http://schemas.microsoft.com/exchange/sensitivity").Value & “;”
WriteFile ContactData
Rs.MoveNext
wend
Rs.Close
Rec.Close
'Clean Up
Set Rs = Nothing
Set Rec = Nothing
End Sub
Sub WriteFile(Contents)
Const ForWriting =2
Const ForAppending = 8
Set oFS = CreateObject("Scripting.FileSystemObject")
Set oFSFile = oFS.OpenTextFile("C:\Contacts.txt",ForAppending,True)
oFSFile.WriteLine(Contents)
oFSFile.Close
Set oFSFile = Nothing
Set oFS = Nothing
End Sub
The list of mailboxes to scan can be provided via a text file(Datafile). The Datafile contains the aliases of the users(one on each line). So assuming your Datafile is called "Aliases.txt" and is on the C:\, you would run the script as follows:
C:\>Cscript DumpContacts.vbs "MyDomain.com" "C:\Aliases.txt"
The account that you are logged on the computer with must have permissions on the mailboxes that you are trying to iterate through. You can give the permissions by following the steps in the article below:
How to assign service account access to all mailboxes in Exchange Server 2003
http://support.microsoft.com/kb/821897/
This is not something everyone would want to do, but just in case below is the sample code that uses ACL.dll (found in the Platform SDK) to set "Reviewer" permissions on all the folders for a specific user.
The following sample is a simple VBScript code sample that iterates through all folders in multiple mailboxes and sets the "Reviewer" permissions. To use this sample, paste the following code in a new text file, and then name the file SetFolderPermissions.vbs:
'This script logs on to a server that is running Exchange Server and iterates through all the mailboxes
'recursively setting the "Reviewer" permission on each folder for a specific user.
' USAGE: cscript SetFolderPermissions.vbs SERVERNAME DATAFILE FullUserName
' This requires that CDO 1.21 and the Acl.dll is installed on the computer.
Dim obArgs
Dim cArgs
Dim objSession
Dim objInfoStores
Dim FullUserName
Set obArgs = WScript.Arguments
cArgs = obArgs.Count
Const CdoMsg = 3,ForReading = 1, ForWriting = 2, ForAppending = 8, TristateFalse = 0
Main
Sub Main()
Dim FileSysObj
Dim DataFileName
Dim DataFile
Dim alias
If cArgs <> 3 Then
WScript.Echo "Usage: cscript SetFolderPermissions.vbs SERVERNAME DATAFILE(Name and Path) FullUserName"
Exit Sub
End If
Set FileSysObj = CreateObject("Scripting.FileSystemObject")
DataFileName = obArgs.Item(1)
FullUserName = obArgs.Item(2)
Set DataFile = FileSysObj.OpenTextFile(DataFileName, ForReading, False,0)
'Read line by line
Do While Not DataFile.AtEndOfStream
alias = DataFile.ReadLine
'Loop through the mailboxes
Call IterateInfoStores(obArgs.Item(0), alias)
Loop
DataFile.Close
'Clean Up
Set DataFile = Nothing
Set FileSysObj = Nothing
End Sub
Sub IterateInfoStores(ServerName,UserName)
Dim objFolder
Dim intCounter
Dim objInfoStore
Dim sMsg
On Error Resume Next
'Create the new Session Object
Set objSession = CreateObject("MAPI.Session")
If Err.Number <> 0 Then
sMsg = "Error creating MAPI.Session."
sMsg = sMsg & "Make sure CDO 1.21 is installed. "
sMsg = sMsg & Err.Number & " " & Err.Description
WScript.Echo sMsg
Exit Sub
End If
'Logon to the Mailbox
objSession.Logon "", "", False, True, 0, False, ServerName & vbLf & UserName
If Err.Number <> 0 Then
sMsg = "Error logging on: "
sMsg = sMsg & Err.Number & " " & Err.Description
WScript.Echo sMsg
WScript.Echo "Server: " & ServerName
WScript.Echo "Mailbox: " & UserName
Set objSession = Nothing
Exit Sub
End If
WScript.Echo "Logged On to:" & objSession.CurrentUser
'Loop through the Infostores
For intCounter = 1 To objSession.InfoStores.Count
Set objInfoStore = objSession.InfoStores(intCounter)
If Err.Number <> 0 Then
sMsg = "Error retrieving InfoStore Object: "
sMsg = sMsg & Err.Number & " " & Err.Description
WScript.Echo sMsg
WScript.Echo "Server: " & ServerName
WScript.Echo "Mailbox: " & UserName
Set objInfoStore = Nothing
Set objSession = Nothing
Exit Sub
End If
If objInfoStore.Name = "Mailbox - " & objSession.CurrentUser Then
Exit For
End If
Next
Set objFolder = objInfoStore.RootFolder
If Err.Number <> 0 Then
sMsg = "Error retrieving RootFolder Object: "
sMsg = sMsg & Err.Number & " " & Err.Description
WScript.Echo sMsg
WScript.Echo "Server: " & ServerName
WScript.Echo "Mailbox: " & UserName
Set objInfoStore = Nothing
Set objFolder = Nothing
Set objSession = Nothing
Exit Sub
End If
'Recurse through the sub-folders
NavigateFolders objFolder
If Err.Number <> 0 Then
sMsg = "Error: "
sMsg = sMsg & Err.Number & " " & Err.Description
WScript.Echo sMsg
WScript.Echo "Server: " & ServerName
WScript.Echo "Mailbox: " & UserName
End If
'Logoff from the session
objSession.Logoff
'Clean Up
Set objFolder = Nothing
Set objInfoStore = Nothing
Set objSession = Nothing
End Sub
Sub NavigateFolders(MAPIFolder)
Dim intCounter
Dim oDelegate
Dim oAddrBook
Dim oNewAce
Dim ACLObj
Dim FolderACEs
Dim objAce
Const ROLE_REVIEWER = &H401
Const ROLE_OWNER = &H5E3
Const ROLE_PUBLISH_EDITOR = &H4E3
Const ROLE_EDITOR = &H463
Const ROLE_PUBLISH_AUTHOR = &H49B
Const ROLE_AUTHOR = &H41B
Const ROLE_NONEDITING_AUTHOR = &H413
Const ROLE_CONTRIBUTOR = &H402
Const ROLE_NONE = &H400
WScript.Echo "Folder Name:" & MAPIFolder.Name
'Create the ACL object
Set ACLObj = CreateObject("MSExchange.aclobject")
' Associate the ACLObject to the CDO Folder
ACLObj.CDOItem = MAPIFolder
Set FolderACEs = ACLObj.ACEs
' Create a MAPI object for UserA
Set oAddrBook = objSession.AddressLists("Global Address List")
Set oDelegate = oAddrBook.AddressEntries.Item(FullUserName)
Set oNewAce = CreateObject("MSExchange.ACE")
oNewAce.ID = oDelegate.ID
oNewAce.Rights = ROLE_REVIEWER
FolderACEs.Add oNewAce
ACLObj.Update
' Loop through all of the ACEs for the folder and display them
For each objAce in FolderACEs
WScript.Echo GetACLEntryName(objAce.ID) & " - " & DispACERules(objAce)
Next
WScript.Echo ""
' Clean up objects
Set objAce = Nothing
Set oNewAce = Nothing
Set FolderACEs = Nothing
Set ACLObj = Nothing
If MAPIFolder.Folders.Count > 0 Then
For intCounter = 1 To MAPIFolder.Folders.Count
NavigateFolders MAPIFolder.Folders(intCounter)
Next
End If
End Sub
Function GetACLEntryName(ACLEntryID)
On Error resume Next
' This function finds the user that is listed as an ACE on the folder.
' It takes the ID that it is passed and uses the Session.GetAddressEntry method
' to find the name.
Dim tmpEntry
Dim tmpName
Select Case ACLEntryID
Case "ID_ACL_DEFAULT"
GetACLEntryName = "Default"
Case "ID_ACL_ANONYMOUS"
GetACLEntryName = "Anonymous"
Case else
' Get the name of the ACE
Set tmpEntry = objSession.GetAddressEntry(ACLEntryID)
tmpName = tmpEntry.Name
GetACLEntryName = tmpName
End Select
End Function
Function DispACERules(DisptmpACE)
' This function checks the roles of the ACE that is passed to it and returns
' the Role back.
Const ROLE_NONE = 1024
Const ROLE_AUTHOR = 1051
Const ROLE_CONTRIBUTOR = 1026
Const ROLE_PUBLISH_AUTHOR = 1179
Const ROLE_NONEDITING_AUTHOR = 1043
Const ROLE_REVIEWER = 1025
Const ROLE_EDITOR = 1147
Const ROLE_OWNER = 2043
Const ROLE_PUBLISH_EDITOR = 1275
' Check the roles on the folder
Select Case DisptmpACE.Rights
Case ROLE_NONE, 0 ' Checking in case the role has not been set on that entry.
DispACERules = "None"
Case ROLE_AUTHOR
DispACERules = "Author"
Case ROLE_CONTRIBUTOR
DispACERules = "Contributor"
Case ROLE_EDITOR
DispACERules = "Editor"
Case ROLE_NONEDITING_AUTHOR
DispACERules = "Nonediting Author"
Case ROLE_OWNER
DispACERules = "Owner"
Case ROLE_PUBLISH_AUTHOR
DispACERules = "Publishing Author"
Case ROLE_PUBLISH_EDITOR
DispACERules = "Publishing Editor"
Case ROLE_REVIEWER
DispACERules = "Reviewer"
Case Else
' This will grab all other custom permissions on the folder
DispACERules = "Custom"
End Select
End Function
The list of mailboxes can be provided via a text file(Datafile). The Datafile contains the aliases of the users(one on each line). So assuming your Datafile is called "Aliases.txt" and is on the C:\, you would run the script as follows:
C:\>Cscript SetFolderPermissions.vbs Exchange2003 C:\Aliases.txt "Akash Bhargava"
The script currently sets and also dumps out the permissions on each folder in the mailbox.
The account that you are logged on the computer with must have permissions on the mailboxes that you are trying to iterate through. You can give the permissions by following the steps in the article below:
How to assign service account access to all mailboxes in Exchange Server 2003
http://support.microsoft.com/kb/821897/
Enjoy!
Having problems in instantiating a new Outlook object from VS 2008(Windows Form Application) when running on Vista with UAC ON? Getting weird errors similar to the ones below?
System.Runtime.InteropServices.COMException was unhandled
Message="Creating an instance of the COM component with CLSID {0006F03A-0000-0000-C000-000000000046} from the IClassFactory failed due to the following error: 80010001."
Source="MyApp"
ErrorCode=-2147418111
This error was when Outlook was NOT running and I tried to debug my application from Visual studio 2008.
System.Runtime.InteropServices.COMException was unhandled
Message="Retrieving the COM class factory for component with CLSID {0006F03A-0000-0000-C000-000000000046} failed due to the following error: 80080005."
Source="MyApp"
ErrorCode=-2146959355
This error was when Outlook was already running and I tried to debug my application from Visual studio 2008. In both cases I got the error when I tried to instantiate a new object of the Outlook.Application.
oApp = new Outlook.Application
Using the Err.exe to get more details on the 80010001 and the 80080005 we get the following details:
# for hex 0x80010001 / decimal -2147418111 :
DBG_EXCEPTION_NOT_HANDLED ntstatus.h
# Debugger did not handle the exception.
RPC_E_CALL_REJECTED winerror.h
# Call was rejected by callee.
# 2 matches found for "80010001"
# for hex 0x80080005 / decimal -2146959355 :
CO_E_SERVER_EXEC_FAILURE winerror.h
# Server execution failed
# 1 matches found for "80080005"
Why would we be getting a RPC_E_CALL_REJECTED or CO_E_SERVER_EXEC_FAILURE? In my case this was happening because of the difference in Integrity Level of the two processes(Outlook.exe and devenv.exe)
The Visual Studio 2008 was being "Run as administrator" which makes it run in High IL and Outlook was by default running in Medium IL and this was causing the errors. The solution is run both the processes in the same IL.
How can you check the Integrity Level of a process? Use Process Explorer(Click on the View menu -> Select Columns->Process Image Tab and check Integrity Level)
Hope this helps!
I have seen many developers wanting to loop through multiple mailboxes either to get the mailbox size or get the number of emails in different folders. Here is a sample that loops through multiple mailboxes recursively.
The following sample is a simple VBScript code sample that iterates through all folders in multiple mailboxes. To use this sample, paste the following code in a new text file, and then name the file Iteratemailboxes.vbs:
'This script logs on to a server that is running Exchange Server and
'iterates through all the mailboxes recursively.
' USAGE: cscript Iteratemailboxes.vbs SERVERNAME DATAFILE
' This requires that CDO 1.21 is installed on the computer.
Dim obArgs
Dim cArgs
Dim objSession
Dim objInfoStores
' Get command line arguments.
Set obArgs = WScript.Arguments
cArgs = obArgs.Count
Const CdoMsg = 3,ForReading = 1, ForWriting = 2, ForAppending = 8, TristateFalse = 0
Main
Sub Main()
Dim FileSysObj
Dim DataFileName
Dim DataFile
Dim alias
If cArgs <> 2 Then
WScript.Echo "Usage: cscript IterateMailboxes.vbs SERVERNAME DATAFILE(Name and Path)"
Exit Sub
End If
Set FileSysObj = CreateObject("Scripting.FileSystemObject")
DataFileName = obArgs.Item(1)
Set DataFile = FileSysObj.OpenTextFile(DataFileName, ForReading, False,0)
'Read line by line
Do While Not DataFile.AtEndOfStream
alias = DataFile.ReadLine
'Loop through the mailboxes
Call IterateInfoStores(obArgs.Item(0), alias)
Loop
DataFile.Close
'Clean Up
Set DataFile = Nothing
Set FileSysObj = Nothing
End Sub
Sub IterateInfoStores(ServerName,UserName)
Dim objFolder
Dim intCounter
Dim objInfoStore
Dim sMsg
On Error Resume Next
'Create the new Session Object
Set objSession = CreateObject("MAPI.Session")
If Err.Number <> 0 Then
sMsg = "Error creating MAPI.Session."
sMsg = sMsg & "Make sure CDO 1.21 is installed. "
sMsg = sMsg & Err.Number & " " & Err.Description
WScript.Echo sMsg
Exit Sub
End If
'Logon to the Mailbox
objSession.Logon "", "", False, True, 0, False, ServerName & vbLf & UserName
If Err.Number <> 0 Then
sMsg = "Error logging on: "
sMsg = sMsg & Err.Number & " " & Err.Description
WScript.Echo sMsg
WScript.Echo "Server: " & ServerName
WScript.Echo "Mailbox: " & UserName
Set objSession = Nothing
Exit Sub
End If
WScript.Echo "Logged On to:" & objSession.CurrentUser
'Loop through the Infostores
For intCounter = 1 To objSession.InfoStores.Count
Set objInfoStore = objSession.InfoStores(intCounter)
If Err.Number <> 0 Then
sMsg = "Error retrieving InfoStore Object: "
sMsg = sMsg & Err.Number & " " & Err.Description
WScript.Echo sMsg
WScript.Echo "Server: " & ServerName
WScript.Echo "Mailbox: " & UserName
Set objInfoStore = Nothing
Set objSession = Nothing
Exit Sub
End If
If objInfoStore.Name = "Mailbox - " & objSession.CurrentUser Then
Exit For
End If
Next
Set objFolder = objInfoStore.RootFolder
If Err.Number <> 0 Then
sMsg = "Error retrieving RootFolder Object: "
sMsg = sMsg & Err.Number & " " & Err.Description
WScript.Echo sMsg
WScript.Echo "Server: " & ServerName
WScript.Echo "Mailbox: " & UserName
Set objInfoStore = Nothing
Set objFolder = Nothing
Set objSession = Nothing
Exit Sub
End If
'Recurse through the sub-folders
NavigateFolders objFolder
'Logoff from the session
objSession.Logoff
'Clean Up
Set objFolder = Nothing
Set objInfoStore = Nothing
Set objSession = Nothing
End Sub
Sub NavigateFolders(MAPIFolder)
Dim intCounter
Dim intMessageCounter
Dim objMessage
On Error Resume Next
WScript.Echo "Folder Name:" & MAPIFolder.Name
For intMessageCounter = 1 To MAPIFolder.Messages.Count
'Implement you own logic here
Set objMessage = MAPIFolder.Messages(intMessageCounter)
Select Case objMessage.Class
Case CdoMsg
Select Case objMessage.Type
Case "IPM.Note"
WScript.Echo "Message:" & objMessage.Subject
Case "IPM.Appointment"
WScript.Echo "Appointment:" & objMessage.Subject
End Select
End Select
Next
If MAPIFolder.Folders.Count > 0 Then
For intCounter = 1 To MAPIFolder.Folders.Count
NavigateFolders MAPIFolder.Folders(intCounter)
Next
End If
Set objMessage = Nothing
End Sub
The list of mailboxes can be provided via a text file(Datafile). The Datafile contains the aliases of the users(one on each line). So assuming your Datafile is called "Aliases.txt" and is on the C:\, you would run the script as follows:
C:\>Cscript iteratemailboxes.vbs Exchange2003 C:\Aliases.txt
The account that you are logged on the computer with must have permissions on the mailboxes that you are trying to iterate through. You can give the permissions by following the steps in the article below:
How to assign service account access to all mailboxes in Exchange Server 2003
http://support.microsoft.com/kb/821897/
Enjoy!
Why do we need to alter the "SetSecurity" and the "Setup" project? I sincerely suggest you read the following Blog posts and article first, otherwise you will not be able to make sense out of this post.
Deploying your VSTO Add-In to All Users (Part I)
Deploying your VSTO Add-In to All Users (Part II)
Automating the Deployment of Outlook Add-ins by Using Visual Studio Tools for Office Second Edition
Assuming you have gone through the articles above I will now walk you through the changes that need to be made in the "Setup" and the "SetSecurity" project. I am only targeting Office 2007 as of now.
Changes to the "Setup" project:
1) Set InstallAllUsers to TRUE.
2) Set the Version Number to the product’s version number.
3) Type your company name in the Manufacturer property.
4) The completed Registry on Target Machine setting for Office 2007 should look like below:

Note: The name of the add-in will differ.
Changes to the "SetSecurity" project:
1) Alter the "Install" method to make sure the CAS policy is configured at the Machine level and not at the User Level. Replace the following line:
bool allUsers = String.Equals(allUsersString, "1");
with
Setting the "allUsers" variable to true makes sure that the CAS policy is created at the Machine level and NOT at the User level.
2) Add the following code to the CaspolSecurityPolicyCreator class in the CaspolSecurityPolicyCreator.cs
//Change the name of the Add-in to suit your needs
private const string userAppLocation = @"Software\Microsoft\Office\12.0\User Settings\OutlookAddIn1";
internal static void IncrementCounter()
{
RegistryKey instructionKey = null;
int count = 1;
try
{
instructionKey = Registry.LocalMachine.OpenSubKey(userAppLocation, true);
object value = instructionKey.GetValue("Count");
if (value != null)
{
if ((int)value != Int32.MaxValue)
count = (int)value + 1;
}
instructionKey.SetValue("Count", count);
}
finally
{
if (instructionKey != null)
instructionKey.Close();
}
}
internal static void RegisterDeleteInstruction()
{
RegistryKey instructionKey = null;
try
{
instructionKey = Registry.LocalMachine.OpenSubKey(userAppLocation, true);
if (instructionKey != null)
{
//Change the name of the Add-in to suit your needs
instructionKey.CreateSubKey(@"Delete\Software\Microsoft\Office\Outlook\AddIns\OutlookAddIn1");
}
}
finally
{
if (instructionKey != null)
instructionKey.Close();
}
}
3) Call the "IncrementCounter" method from the Install method.
4) Call the "IncrementCounter" and the "RegisterDeleteInstruction" methods from the Uninstall method.
That's all the changes that are needed. I have tried the setup on a Windows XP(Office 2007) machine and it WORKS!
Its better late than never, I have been wanting to write this post for a long time now and looks like the time has finally come! This is about adapting the "SetSecurity" project to grant Full Trust to the add-in installation folder instead of just the add-in assembly.
What is "SetSecurity"? This is a sample project that comes with the Deploying Office Solutions Using Windows Installer Version 3 Sample. This project is used to Grant trust to the customization assembly using a custom action. Want to know more? Read the following articles about Deploying Visual Studio 2005 Tools for Office Second Edition Solutions and Granting Permissions.
Deploying Visual Studio 2005 Tools for Office Second Edition Solutions Using Windows Installer (Part 1 of 2)
http://msdn.microsoft.com/en-us/library/bb332051.aspx
Deploying Visual Studio 2005 Tools for Office Second Edition Solutions Using Windows Installer: Walkthroughs (Part 2 of 2)
http://msdn.microsoft.com/en-us/library/bb332052.aspx
How to: Grant Permissions to Folders and Assemblies
http://msdn.microsoft.com/en-us/library/zdc263t0(VS.80).aspx
Why do I need to grant full trust to an add-in installation folder instead of the assembly? If you add-in is simple and contains only one assembly then this is not required. You will need to do this in case you have other assemblies that are referenced in you project and they are deployed in the Installation folder along with your add-in assembly.
If only the add-in assembly is trusted you add-in with throw a security exception when it tries to load the other assemblies because they are not trusted. if we trust the installation folder all the assemblies in that folder are trusted and the add-in works fine.
There is very small change we that we need to make and it need to made to the AddSecurityPolicy method in the "CaspolSecurityPoliceCreator.cs" file.
Below are the changes:
1)Replace the following line:
string arguments = policyLevel + " -q -ag " + parentCodeGroup + " -url \"" + solutionInstallationUrl + "\" Nothing -n \"" + solutionCodeGroupName + "\" -d \"" + solutionCodeGroupDescription + "\"";
With
string arguments = policyLevel + " -q -ag " + parentCodeGroup + " -url \"" + solutionInstallationUrl + "\" FullTrust -n \"" + solutionCodeGroupName + "\" -d \"" + solutionCodeGroupDescription + "\"";
2) Comment out or delete the following try..catch block:
// Add the assembly code group. Grant FullTrust permissions to the main assembly.
try
{
// Use the assembly strong name as the membership condition.
// Ensure that the assembly is strong-named to give it full trust.
AssemblyName assemblyName = Assembly.LoadFile(assemblyPath).GetName();
arguments = policyLevel + " -q -ag \"" + solutionCodeGroupName + "\" -strong -file \"" + assemblyPath + "\" \""
+ assemblyName.Name + "\" \"" + assemblyName.Version.ToString(4) + "\" FullTrust -n \""
+ assemblyCodeGroupName + "\" -d \"" + assemblyCodeGroupDescription + "\"";
RunCaspolCommand(frameworkFolder, arguments);
}
catch (Exception ex)
{
try
{
// Clean the solutionCodeGroupName.
RemoveSecurityPolicy(machinePolicyLevel, solutionCodeGroupName);
}
catch { }
string error = String.Format("Cannot create the security code group '{0}'.", assemblyCodeGroupName);
throw new Exception(error, ex);
}
These are the only two changes we need to make. The first change grants the solutioninstallationUrl "FullTrust" instead of "Nothing" and the second change deletes the code that grants trust to the add-in assembly.
The rest is covered in the Deploying Visual Studio 2005 Tools for Office Second Edition Solutions Using Windows Installer: Walkthroughs (Part 2 of 2) article mentioned above.