Why can SmtpClient.SendAsync only be called once?
Asked Answered
K

7

22

I'm trying to write a notification service (for completely legit non-spam purposes) in .NET using SmtpClient. Initially I just looped through each message and sent it, however this is slow and I would like to improve the speed. So, I switched to using 'SendAsync', but now get the following error on the second call:

An asynchronous call is already in progress. 

I read this to mean that MS crippled System.Net.Mail to prevent mass-mailers. Is this correct? If so, is there a better way to do this in .NET, and still be able to log the results of each email(which is important to our client). If not, why can SendAsync only be called once?

Kristine answered 23/12, 2008 at 18:38 Comment(0)
T
36

According to the documentation:

After calling SendAsync, you must wait for the e-mail transmission to complete before attempting to send another e-mail message using Send or SendAsync.

So to send multiple mails at the same time you need multiple SmtpClient instances.

Treacherous answered 23/12, 2008 at 18:43 Comment(5)
Doesn't anyone already implemented such a mechanism to send multiple emails at once? on top of the smtp client class? sounds like a problem alot of people encounter... kind of weird that no one ever created some framework for it.Kamin
@ItayLevin I dont see a need for attempting to reuse the SmtpClient instance. Why not just dedicate each instance for each email send attempt as Darin expertly explained it.Imprinting
This is a good post on the topic.Lankford
The problem is, if you have multiple instances, you can get blocked from the SMTP server because of too many connections. Sharing the instance allows you to send multiple messages through one connection without running into that. I have my solution, or work around if you wish.Sinner
@PBMe_HikeIt: dead link. Here goes an archived version: web.archive.org/web/20181113075158/http://www.codefrenzy.net:80/…Ludwig
C
6

You might be able to use the following:

ThreadPool.QueueUserWorkItem(state => client.Send(msg));

This should allow your messages to be queued and sent as threads become available.

Callow answered 1/9, 2009 at 19:54 Comment(1)
This implementation still may produce a System.InvalidOperationException when sending from the same SmtpClient instance.Maurizia
N
5

Obviously, this is not an attempt to stop mass mailers.

The reason is that the SmtpClient class is not thread safe. If you want to send multiple emails simultaneously, you have to spawn a few worker threads (there are a few ways to do that in the .NET Framework) and create separate instances of SmtpClient in each of them.

Nall answered 23/12, 2008 at 18:42 Comment(0)
S
5

I think you misunderstand the XXXAsync class of methods. The purpose of these asynchronous calls is to allow the program to continue running, without the need of the method to finish processing and return first. You can then proceed with the result later by subscribe to something like XXXReceived event of the object.

To send more than one mail simultaneously, you may consider using more Threads.

Sedillo answered 23/12, 2008 at 18:46 Comment(3)
Multiple threads calling the SendAsync is exactly what causes this exceptionDoane
@statichippo The OP didn't use async version of send mail in the first place. what I suggested is to use multiple threads with non-async call.Sedillo
If you call Send (non-async) on multiple threads you will get an exception. The SmtpClient class is only capable of sending 1 message at a time regardless of how many threads are sending it. It's a protocol issue, not a C# issue really... You have to wait for one message to get sent before sending another -- regardless of whether you're using SendAsync or Send. Unless you want to create mutliple SmtpClientsDoane
T
2

You may only send one at a time per SMTP client. If you wish to make more than one send call, create more than one SMTP client.

HTH,

Colby Africa

Therontheropod answered 23/12, 2008 at 18:43 Comment(0)
H
2

As noticed by everyone else here, you can only send one email at a time, but the way to send another once the first has been sent is to handle the .SendCompleted event of the SmtpClient class, and then move on to the next email and send that.

If you want to send many emails simultaneously, then as the others have said, use multiple SmtpClient objects.

How answered 23/12, 2008 at 18:52 Comment(0)
S
2

There is a reason to reuse the SmtpClient, it limits the # of connections to the SMTP server. I cannot instantiate a new class SmtpClient class for each thread the reports are building on or the SMTP server will balk with too many connections error. This is the solution I came up with when I couldn't find an answer here.

I ended up using an AutoResetEvent for keeping everything in sync. That way, I can keep calling my SendAsync in each thread, but wait for it to process the email and use the SendComplete event to reset it so the next one can continue.

I setup the Auto Reset Event.

        AutoResetEvent _autoResetEvent = new AutoResetEvent(true);

I setup the shared SMTP Client when my class is instantiated.

        _smtpServer = new SmtpClient(_mailServer);
        _smtpServer.Port = Convert.ToInt32(_mailPort);
        _smtpServer.UseDefaultCredentials = false;
        _smtpServer.Credentials = new System.Net.NetworkCredential(_mailUser, _mailPassword);
        _smtpServer.EnableSsl = true;
        _smtpServer.SendCompleted += SmtpServer_SendCompleted;

Then when I call the send async, I wait for the event to clear, then send the next one.

        _autoResetEvent.WaitOne();
        _smtpServer.SendAsync(mail, mail);
        mailWaiting++;

I use the SMTPClient SendComplete event to reset the AutoResetEvent so the next email will send.

private static void SmtpServer_SendCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
        {
            MailMessage thisMesage = (MailMessage) e.UserState;
            if (e.Error != null)
            {
                if (e.Error.InnerException != null)
                {
                    writeMessage("ERROR: Sending Mail: " + thisMesage.Subject + " Msg: "
                                 + e.Error.Message + e.Error.InnerException.Message);
                }

                else
                {
                    writeMessage("ERROR: Sending Mail: " + thisMesage.Subject + " Msg: " + e.Error.Message);
                }
            }
            else
            {
                writeMessage("Success:" + thisMesage.Subject + " sent.");
            }
        if (_messagesPerConnection > 20)
        {  /*Limit # of messages per connection, 
            After send then reset the SmtpClient before next thread release*/
            _smtpServer = new SmtpClient(_mailServer);
            _smtpServer.SendCompleted += SmtpServer_SendCompleted;
            _smtpServer.Port = Convert.ToInt32(_mailPort);
            _smtpServer.UseDefaultCredentials = false;
            _smtpServer.Credentials = new NetworkCredential(_mailUser, _mailPassword);
            _smtpServer.EnableSsl = true;
            _messagesPerConnection = 0;
        }
            _autoResetEvent.Set();//Here is the event reset
            mailWaiting--;
        }
Sinner answered 12/6, 2018 at 14:46 Comment(1)
If you are on .NET 4.0 or above, you should have a look at SemaphoreSlim or ManualResetEventSlim which are faster as they do not rely on the Win32 kernel objects (learn.microsoft.com/en-us/dotnet/standard/threading/…).Death

© 2022 - 2024 — McMap. All rights reserved.