1

This is my first time asking a question on StackOverflow, so let me know if you need more clarification. I am attempting to send emails with an attachment to users of a system, however I am having difficulty cleaning up the attachments on the file system after the messages are sent.

Background

I am using SSIS to compile a listing of email recipients and message content from a SQL database, then using a Script Component to do the actual email sending with C#. I am more of a DBA/Project Manager by role, and know enough coding to do some small things, but it has been quite some years since I did any .Net coding on a daily basis.

Each time I run the routine, the attachment files (Excel files in this case) are created successfully in a dedicated directory, then the emails are generated using those attachments and placed in a pickup folder. As each email is created and sent, I want to delete the attachment from its directory, but I get this error:

The process cannot access the file 'D:\mailtest\{filename}.xls' because it is being used by another process.

The specific file that it errors on is different each time, and of the ~3000 emails that are to be generated, it fails at around the 1200-1500 emails mark.

Code

        //Create message
    using (MailMessage msg = new MailMessage())
    {
        msg.To.Add(new MailAddress(EmailRecipient));
        if (Row.Cc.Length > 0)
        {
            msg.CC.Add(new MailAddress(Row.Cc));
        }
        if (Row.Bcc.Length > 0)
        {
            msg.Bcc.Add(new MailAddress(Row.Bcc));
        }
        msg.From = new MailAddress(EmailSender);
        msg.Subject = MessageSubject;
        msg.Body = MessageBody +
            "\n" +
            "\n" +
            this.Variables.EmailFooter;
        msg.IsBodyHtml = true;
        msg.BodyEncoding = System.Text.Encoding.UTF8;
        //Add attachment data        
        if (File.Exists(attachmentPath))
        {
            Attachment data = new Attachment(attachmentPath);
            data.ContentDisposition.FileName = "Expiring Training.xls";
            msg.Attachments.Add(data);
        }
        if (this.Variables.UsePickupDirectory)  //Drops items into pickup directory
        {
            SmtpClient client = new SmtpClient(SMTPEndPoint, SMTPPort)
            {
                EnableSsl = false,
                DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory,
                PickupDirectoryLocation = this.Variables.PickupDirectory,
                Credentials = new NetworkCredential(UserName, Password)
            };
            try
            {
                client.Send(msg);
            }
            catch (Exception e)
            {
                //Debug.WriteLine(e);
                Row.ErrorMessage = e.Message;
            }
            finally
            {
                msg.Dispose();  //Release file
            }
        }
        else //Send to SMTP Server
        {
            SmtpClient client = new SmtpClient(SMTPEndPoint, SMTPPort)
            {
                EnableSsl = true,
                DeliveryMethod = SmtpDeliveryMethod.Network,
                Credentials = new NetworkCredential(UserName, Password)
            };
            try
            {
                client.Send(msg); 
            }
            catch (Exception e)
            {
                //Debug.WriteLine(e);
                Row.ErrorMessage = e.Message;
            }
            finally
            {
                //Add sleep to prevent sending more than 10 emails per second
                System.Threading.Thread.Sleep(100);
                msg.Dispose();  //Release file
            }
        }
    }
    //Remove attachment file
    if (File.Exists(attachmentPath))
    {
        File.Delete(attachmentPath);
    }

Things I've Tried

  • Initially, I did not have the using block. I saw this suggestion on a similar SO question, but adding it here seems to make no difference.
  • I've added the Dispose() to the finally block of each path (only the pickup directory is used in this case), which I understand should release all locks on the files used as attachments.
  • Without either of those things, it fails on the first file encountered, which leads me to believe it is working, but only for a while, and then fails suddenly at a random point during the execution.
  • The file specified in the error is not showing in process explorer when I search for it, so maybe it is released quickly after the error, so that I cannot search in time?
  • I tried moving the "Delete" functionality to a separate process entirely, running directly after all emails had been sent, but the same message would appear anyway.

I'd be thankful for any advice if anyone has a clue what could be happening.

New info

Adding some extra error handling did improve things, but didn't totally fix it. I've had several runs make it all the way through successfully, and whenever there was an error, it was only on 1 out of the ~3000 files. It did show the same error, though: "The process cannot access the file 'D:\mailtest{...}.xls' because it is being used by another process." That error was from the File.Delete line. It makes me wonder how just by adding more stuff it errors less. Is the sending and deleting happening so fast that it's stepping on its own toes? And throwing stuff in slows it down enough that it's able to keep up?

New Info 2

I took Jamal's advice and added a 500ms delay between send and delete, but only if the first delete attempt failed. So far no errors on 10 straight runs, whereas it was failing every single run in some way prior to adding it. However, the FireInformation message never appeared in the output window, leading me to think it never reached that block, so I'm not sure why adding it seems to work.

        //Remove attachment file
    if (File.Exists(attachmentPath))
    {
        try
        {
            File.Delete(attachmentPath);
        }
        catch
        {
            try
            {
                this.ComponentMetaData.FireInformation(0, "Delete failed", "Trying Delete again after 500ms", "", 0, fireAgain);
                System.Threading.Thread.Sleep(500);
                File.Delete(attachmentPath);
            }
            catch (Exception e)
            { 
                Row.ErrorMessage = e.Message;
                this.ComponentMetaData.FireInformation(e.HResult, e.Source, e.Message, e.HelpLink, 0, fireAgain);
            }
        }
    }
Agbullet
  • 31
  • 6
  • 1
    Is excel opened? Do you see excel in Task Manager? I'm suspecting it is not your code and maybe excel. – jdweng Jul 08 '20 at 16:43
  • @jdweng, I don't have Excel open, and I just ran it again to see if it snuck open in the background, but it never appeared in task manager. – Agbullet Jul 08 '20 at 16:47
  • Did you try to dispose client? – jdweng Jul 08 '20 at 16:53
  • @jdweng, I think I did that as one of my troubleshooting steps, but prior to adding the using block. I'll give it a whirl now and see if that helps. – Agbullet Jul 08 '20 at 16:56
  • @jdweng Added client.Dispose() just prior the the msg.Dispose() line. Still errors in the same way. – Agbullet Jul 08 '20 at 17:07
  • Put a try/get inside the entire using block and see if you get the exception. – jdweng Jul 08 '20 at 17:15
  • `Attachment` (`AttachmentBase`) provides a `.Dispose()` method. Declare that too with a `using` statement. When you declare a disposable object with a `using` statement, you don't need to call `Dispose()`. That's what `using` is for. – Jimi Jul 08 '20 at 17:20
  • `SmtpClient` is also disposable. All your disposable object need to be disposed of. -- Note that's this class is obsolete. You should migrate to [MailKit](https://github.com/jstedfast/MailKit) or similar. – Jimi Jul 08 '20 at 17:30
  • @jdweng Added a try/catch for the whole using block, but same error appears as above. – Agbullet Jul 08 '20 at 17:49
  • Possible duplicate: https://stackoverflow.com/questions/1296380/smtp-send-is-locking-up-my-files-c-sharp – Jamal Jul 08 '20 at 17:49
  • Does stack trace indicate which line? – jdweng Jul 08 '20 at 17:55
  • @Jimi Interesting, was not aware it was obsolete. I just "borrowed" the base of it from a blog post that was doing something similar to what I was trying to accomplish. Wrapping everything in usings does seem to be a bit of a clunky fix, but I'm at the point where the faster solution is the winner. If I can get this working using SmtpClient, then yay, but otherwise I fear rewriting will cost me time which I don't have a lot of at the moment. – Agbullet Jul 08 '20 at 18:02
  • @jdweng not specifically due to the quirks of SSIS, but the error disappears when I comment out the Delete command, so pretty sure that's the line. – Agbullet Jul 08 '20 at 18:06
  • @Jamal I saw that thread as well and tried the answers in it, including disposing the message and adding the using block, but still have the issue, so wouldn't this be different? – Agbullet Jul 08 '20 at 18:10
  • You need to add more exception handler to isolate. When an exception occurs in the managed library and NO exception handler is found the code moves up the execution stack until first handler is found (even if it is doesn't surround the error). So error could be before the using statement. – jdweng Jul 08 '20 at 18:12
  • @jdweng I'll give it a shot. Will report back if I discover anything new. – Agbullet Jul 08 '20 at 18:18
  • It looks like by adding some extra error handling, it actually improved things dramatically. I've had several runs make it all the way through, and whenever there was an error, it was only on 1 out of the ~3000 files. It did show the same error, though " – Agbullet Jul 08 '20 at 20:34
  • @Agbullet, can you try increasing sleep to the System.Threading.Thread.Sleep(500) ? This may give the objects sometime to be garbage collected and disposed of. – Jamal Jul 09 '20 at 04:03
  • @Jamal, I took your idea and added that as a "second chance" delete in a nested try block, and so far I'm having 100% success. However, I added a FireInformation line to show if the second chance was ever used, and so far through 10 runs it hasn't actually reached that branch at all. So adding the line seems to have fixed it, even though it's never actually reached in execution. Not really sure if that counts as a solution, but it's working for me, so I'd count it as an answer. – Agbullet Jul 09 '20 at 15:53

0 Answers0