1

I'm trying to develop an app that sends email, and our internal network is locked down pretty tight, so I can't relay using our internal mail servers.

Has anyone ever used something like no-ip.com? Are there any other alternatives?

chris
  • 36,094
  • 53
  • 157
  • 237
  • What exactly are you trying to do? – SLaks Feb 04 '10 at 17:59
  • User submits a form, we send out a confirmation email. Not rocket science. – chris Feb 04 '10 at 18:10
  • Write your own SMTP server that runs on your PC. –  Feb 04 '10 at 18:27
  • @Changeling, do you think he should do that before or after reinventing the wheel? – Steven Sudit Feb 04 '10 at 19:31
  • 1
    Wouldn't make a difference, the problem is that port 25 is blocked. – chris Feb 04 '10 at 19:42
  • @Steven: Well, if he can write apps on his machine but can't install anything, it is a viable solution. Get a grip. –  Feb 04 '10 at 20:17
  • If port 25 weren't blocked, you could set the Host (either programmatically or through app.config) to the MX server for the destination domain, avoiding the intermediate step of a gateway server. This ignores the problem of doing MX lookups, retries and all the other tedious tasks that any SMTP server will do for you, but it might be good enough for testing. If port 25 is blocked, you can likewise set the Port property to 80 or something, but you'll need to have a proxy on the other end to switch it back to 25. What a mess! – Steven Sudit Feb 04 '10 at 20:36

5 Answers5

4

If you just need to check that the e-mails are being sent to the correct addresses and with the correct contents, the easiest way is to use a drop folder by editing the app or web.config file:

  <system.net>
    <mailSettings>
      <smtp deliveryMethod="SpecifiedPickupDirectory" from="me@myorg.com">
        <specifiedPickupDirectory pickupDirectoryLocation="C:\TestMailDrop"/>
      </smtp>
    </mailSettings>
  </system.net>

This will result in the e-mails being created as files in the specified directory. You can even then load the files and verify them as part of a unit test.

(As codekaizen points out, this can also be done in code if you don't mind modifying the code/hardcoding the drop folder and having the behavior differing in debug/release mode.)

technophile
  • 3,556
  • 1
  • 20
  • 24
  • sounds like the best option for now - that, plus it's obviously time to put my resume up on careers.stackoverflow.com :) – chris Feb 04 '10 at 19:25
  • I'm going to restrict myself to technical advice here, instead of career advice. But, yes, if your employer wants you to do something and then works to stop you from doing it, you've got a problem. – Steven Sudit Feb 04 '10 at 20:50
  • +0 except as a short term trick with non-automated testing, this doesnt end up very workable, esp with an absolute path. Even if you have a relative one, you want to a) stop it overflowing b) clean it so you can be sure you're not looking at stale files – Ruben Bartelink Aug 10 '12 at 11:33
  • Easily solved with a test setup and clean up task to clean the directory – Uriah Blatherwick Aug 04 '13 at 21:25
2

You can save the email to disk:

#if DEBUG
smtpClient.PickupDirectoryLocation = "\\Path\\to\\save\\folder";
smtpClient.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory;
smtpClient.Send(msg);
#endif
codekaizen
  • 26,990
  • 7
  • 84
  • 140
  • This works but I'm more comfortable with the app.config solution, since it's not tied to debug or release builds. – Steven Sudit Feb 04 '10 at 20:29
  • Yea, that's the better way... I had just forgot how to do the config trick. – codekaizen Feb 04 '10 at 21:37
  • 1
    +1 You dont want hardwired paths in a config file or it'll be a nightmare to maintain. You should be creating a temporary directory fixture, wiping it down after tests and asserting against its contents – Ruben Bartelink Aug 10 '12 at 11:22
1

Riffing from @codekaizen, using AutoFixture.xUnit [which is available as an XUnit package by that name]:-

    [Theory, AutoData]
    public static void ShouldSendWithCorrectValues( string anonymousFrom, string anonymousRecipients, string anonymousSubject, string anonymousBody )
    {
        anonymousFrom += "@b.com";
        anonymousRecipients += "@c.com";

        using ( var tempDir = new TemporaryDirectoryFixture() )
        {
            var capturingSmtpClient = new CapturingSmtpClientFixture( tempDir.DirectoryPath );
            var sut = new EmailSender( capturingSmtpClient.SmtpClient );

            sut.Send( anonymousFrom, anonymousRecipients, anonymousSubject, anonymousBody );
            string expectedSingleFilename = capturingSmtpClient.EnumeratePickedUpFiles().Single();
            var result = File.ReadAllText( expectedSingleFilename );

            Assert.Contains( "From: " + anonymousFrom, result );
            Assert.Contains( "To: " + anonymousRecipients, result );
            Assert.Contains( "Subject: " + anonymousSubject, result );
            Assert.Contains( anonymousBody, result );
        }
    }

CapturingSmtpClientFixture is only used in a test context-

    class CapturingSmtpClientFixture
    {
        readonly string _path;
        readonly SmtpClient _smtpClient;

        public CapturingSmtpClientFixture( string path )
        {
            _path = path;
            _smtpClient = new SmtpClient { DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory, PickupDirectoryLocation = _path };
        }

        public SmtpClient SmtpClient
        {
            get { return _smtpClient; }
        }

        public IEnumerable<string> EnumeratePickedUpFiles()
        {
            return Directory.EnumerateFiles( _path );
        }
    }

All you need to do then is make sure your actual code supplies an SmtpClient that has been wired up with parameters appropriate to the live SMTP server.

(For completeness, here is TemporaryDirectoryFixture):-

public class TemporaryDirectoryFixture : IDisposable
{
    readonly string _directoryPath;

    public TemporaryDirectoryFixture()
    {
        string randomDirectoryName = Path.GetFileNameWithoutExtension( Path.GetRandomFileName() );

        _directoryPath = Path.Combine( Path.GetTempPath(), randomDirectoryName );

        Directory.CreateDirectory( DirectoryPath );
    }

    public string DirectoryPath
    {
        get { return _directoryPath; }
    }

    public void Dispose()
    {
        try
        {
            if ( Directory.Exists( _directoryPath ) )
                Directory.Delete( _directoryPath, true );
        }
        catch ( IOException )
        {
            // Give other process a chance to release their handles
            // see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true/1703799#1703799
            Thread.Sleep( 0 );
            try
            {
                Directory.Delete( _directoryPath, true );
            }
            catch
            {
                var longDelayS = 2;
                try
                {
                    // This time we'll have to be _really_ patient
                    Thread.Sleep( TimeSpan.FromSeconds( longDelayS ) );
                    Directory.Delete( _directoryPath, true );
                }
                catch ( Exception ex )
                {
                    throw new Exception( @"Could not delete " + GetType() + @" directory: """ + _directoryPath + @""" due to locking, even after " + longDelayS + " seconds", ex );
                }
            }
        }
    }
}

and a skeleton EmailSender:

public class EmailSender
{
    readonly SmtpClient _smtpClient;

    public EmailSender( SmtpClient smtpClient )
    {
        if ( smtpClient == null )
            throw new ArgumentNullException( "smtpClient" );

        _smtpClient = smtpClient;
    }

    public void Send( string from, string recipients, string subject, string body )
    {
        _smtpClient.Send( from, recipients, subject, body );
    }
}
Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249
  • I tried using your TemporaryDirectoryFixture as AutoData. The Dispose method gets called by Xunit, but I get an IOException Access to the path is denied. It works if I have a using statement inside the test. Maybe the Xunit runner has different permissions than the test code which would cause this? – Eric Roller Jul 27 '15 at 05:10
  • @EricRoller Is that v2 or v1? In both, the tests run inside an AppDomain (though v2 has recently added a switch to allow this to be inhibited). However the ways in parameters will be created and/or `Dispose`d vary significantly between v1 and v2. In v1, Disposal of args didnt happen without lots of trickery (Custom `FactAttribute`s etc.). I have not spent enough time reading the docs on what v2 does with `Disposable`s to know what to expect other than to say that doing a `using` is going to be easy to understand unless you have a significant no of tests benefitting enough to make it worthwhile – Ruben Bartelink Jul 27 '15 at 08:05
  • I am using v2 and just figured out what was happening. I had modified TemporaryDirectoryFixture to extend DirectoryInfo. Autofixture was then calling all the public setters with random data which somehow caused the access denied when deleting the directory. I fixed that by overriding all the public setters with no-op. Thanks for the TemporaryDirectoryFixture code, it makes writing small integration tests really quick. – Eric Roller Jul 28 '15 at 11:34
  • @EricRoller Interesting; glad to hear you're sorted – Ruben Bartelink Jul 28 '15 at 11:47
0

The usual answer is to run SMTP locally under IIS, but you need to be careful about who you're sending to. It might actually be better to send to your usual SMTP server and target only accounts within your domain.

Steven Sudit
  • 19,391
  • 1
  • 51
  • 53