diff --git a/README b/README new file mode 100644 index 0000000..f315682 --- /dev/null +++ b/README @@ -0,0 +1,73 @@ +Simple SMTP Mailer +Written by Richard Walmsley +WWW: http://walmsley.gen.nz/ + +Description +=========== +A simple API used to send out e-mails. It supports attachments but lacking authentication as it wasn't required for my use. +It's Windows only due to the use of its DNS lookup functions, although outside of those, it should be easy to port but as there's far more advance tools out there, it's not worth the use. +No tested but should be thread-safe. + +Included an example. + +API Usage +========= +All functions return SMTP_ERR_SUCCESS (0) on success. + +The error codes are as follows; +* SMTP_ERR_INVALID_STATE - Function called without a successful call to a prerequisite function. +* SMTP_ERR_FAILURE - Server returned a non-success code or unable to connect to server. +* SMTP_ERR_BUFFER - Out of memory or static buffer too small. The latter shouldn't happen with a valid e-mail address. +* SMTP_ERR_PROTOCOL - Protocol error. Server not following the spec or there's a transfer error. +* SMTP_ERR_DATA - The data passed to the function is invalid. + +The socket used has a receive timeout of 15 seconds. This can be adjusted by defining SMTP_BLOCKING_TIME before including ssmtp.h. The value is in milliseconds. + +int SMTPConnect(SMTPConn *Conn, const char *Domain, const char *HeloLine); +-------------------------------------------------------------------------- +Attempts to connects to the most suitable mail server. + +Before calling this function for the first use of a SMTPConn, ensure that SMTPConn->State is set to 0 otherwise the function may fail with SMTP_ERR_INVALID_STATE. It only needs to be set once per SMTPConn as it is then handled internally. + +This looks up the MX records for the domain passed and then attempts to connect to them sorted by their preference. If one fails, it'll continue to the next one. If none of them work, it'll try to connect to the server's A records as per the spec. + +Once connected, it'll send through the HELO line using the passed string. If the server returns an unsuccessful code, it'll disconnect and continue through the list. + +int SMTPAddress(SMTPConn *Conn, int Type, const char *Address); +--------------------------------------------------------------- +Adds a single e-mail address to the buffer. + +The types are as follows; +* SMTP_ADDRESS_FROM +* SMTP_ADDRESS_TO +* SMTP_ADDRESS_CC +* SMTP_ADDRESS_BCC + +There's no limit other then the available memory. BCC addresses aren't included into its buffer. +The senders' address always needs to be sent first otherwise the function will fail with SMTP_ERR_INVALID_STATE. + +The address can be in either format; 'test@example.org' or '"Testing Account" '. +There is no checking to see if an address has been added twice. + +int SMTPData(SMTPConn *Conn, const char *Subject, const char *Body, SMTPAttach *Attachments); +--------------------------------------------------------------------------------------------- +Sends off the e-mail. + +Before calling this, we must have called SMTPAddress() at least twice returning successful. First with a sender and then again with a receiver. + +The subject line is optional. If Attachments is NULL, the e-mail will be a standard e-mail, without MIME. +See the included example for further infomation on attachments. + +int SMTPReset(SMTPConn *Conn); +------------------------------ +Sends the SMTP RSET command. This clears any addresses that may have been added to the buffer. As if starting with a fresh connection. + +int SMTPDisconnect(SMTPConn *Conn); +----------------------------------- +Disconnects from the mail server. + +This will free any memory used by the address buffer. The SMTPConn can then be reused. + +License +======= +Distributed under the MIT License. See the included LICENSE for details. diff --git a/base64.c b/base64.c new file mode 100644 index 0000000..88870cc --- /dev/null +++ b/base64.c @@ -0,0 +1,106 @@ +/* + An error-free in-memory Base64 encoder. + + Simple SMTP Mailer. + Copyright (C) 2013 Richard Walmsley + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include /* For memcpy() */ + +#include "base64.h" + +static const char B64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static void EncodeBlock(unsigned char In[BASE64_IN_SIZE], unsigned char Out[BASE64_OUT_SIZE], unsigned int Length) +{ + unsigned int Index; + + for (Index = Length; Index < BASE64_IN_SIZE; Index++) + In[Index] = 0; + + Out[0] = B64[In[0] >> 2]; + Out[1] = B64[((In[0] & 0x03) << 4) | ((In[1] & 0xF0) >> 4)]; + Out[2] = (unsigned char)(Length > 1 ? B64[((In[1] & 0x0F) << 2) | ((In[2] & 0xC0) >> 6)] : '='); + Out[3] = (unsigned char)(Length > 2 ? B64[In[2] & 0x3F] : '='); + + return; +} + +void InitEncode64(B64Stream *Stream) +{ + Stream->AvailIn = Stream->AvailOut = \ + Stream->TotalIn = Stream->TotalOut = \ + Stream->BlockSize = Stream->BlockOut = 0; + + return; +} + +void Encode64(B64Stream *Stream, int Finished) +{ + unsigned char OutBlock[BASE64_OUT_SIZE]; + + /* Loop until we have no more input or unable to output. */ + while (Stream->AvailIn != 0 || Stream->BlockSize != 0) { + + /* If anything is in our block for output, dump it. */ + if (Stream->BlockOut && Stream->BlockSize != 0) { + + for (; Stream->BlockSize != 0; Stream->BlockSize--) { + + if (Stream->AvailOut == 0) + return; + + *Stream->NextOut = Stream->Block[BASE64_OUT_SIZE - Stream->BlockSize]; + Stream->NextOut++; + Stream->TotalOut++; + Stream->AvailOut--; + } + + } + + /* Now that our buffer is empty, fill the input. */ + Stream->BlockOut = 0; + for (; Stream->BlockSize < BASE64_IN_SIZE; Stream->BlockSize++) { + + /* Out of input. This may be expected if there is no data left. */ + if (Stream->AvailIn == 0) { + if (!Finished || Stream->BlockSize == 0) + return; + break; + } + + Stream->Block[Stream->BlockSize] = *Stream->NextIn; + Stream->NextIn++; + Stream->TotalIn++; + Stream->AvailIn--; + } + + /* Encode the input we have. */ + EncodeBlock(Stream->Block, OutBlock, Stream->BlockSize); + + Stream->BlockSize = BASE64_OUT_SIZE; + memcpy(Stream->Block, OutBlock, Stream->BlockSize); + + Stream->BlockOut = 1; + } + + return; +} diff --git a/base64.h b/base64.h new file mode 100644 index 0000000..49a0942 --- /dev/null +++ b/base64.h @@ -0,0 +1,25 @@ +#ifndef BASE64_H +#define BASE64_H + +#define BASE64_OUT_SIZE 4 +#define BASE64_IN_SIZE 3 + +typedef struct B64Stream { + unsigned int AvailIn; + unsigned int TotalIn; + unsigned char *NextIn; + + unsigned int AvailOut; + unsigned int TotalOut; + char *NextOut; + + /* Internal cache. */ + unsigned int BlockSize; + int BlockOut; /* Direction of block. (Input or Output) */ + unsigned char Block[BASE64_OUT_SIZE]; +} B64Stream; + +void InitEncode64(B64Stream *Stream); +void Encode64(B64Stream *Stream, int Finished); + +#endif diff --git a/cbuffer.c b/cbuffer.c new file mode 100644 index 0000000..61fe9a9 --- /dev/null +++ b/cbuffer.c @@ -0,0 +1,113 @@ +/* + Simple set of functions used to prevent sending small amounts of data. + + Simple SMTP Mailer. + Copyright (C) 2013 Richard Walmsley + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include +#include + +#include "cbuffer.h" + +int CFlush(CSendBuffer *Buffer) +{ + int Return; + + if (Buffer->Cursor > 0) { + + Return = Buffer->Callback(Buffer->CallbackData, Buffer->Data, Buffer->Cursor); + if (Return != 0) + return Return; + Buffer->Cursor = 0; + + } + + return 0; +} + +int CSend(CSendBuffer *Buffer, const char *Data, unsigned int Size) +{ + unsigned int BufferAvailable; + int Return; + + while (Size > 0) { + + BufferAvailable = Buffer->Size - Buffer->Cursor; + if (BufferAvailable > Size) + BufferAvailable = Size; + + memcpy(&Buffer->Data[Buffer->Cursor], Data, BufferAvailable); + + Size -= BufferAvailable; + Data += BufferAvailable; + + Buffer->Cursor += BufferAvailable; + if (Buffer->Cursor >= Buffer->Size) { + + Return = Buffer->Callback(Buffer->CallbackData, Buffer->Data, Buffer->Cursor); + if (Return != 0) + return Return; + Buffer->Cursor = 0; + } + + } + + return 0; +} + +int CSendStrings(CSendBuffer *Buffer, ...) +{ + va_list Args; + char *Arg; + size_t Length; + int Return; + + va_start(Args, Buffer); + + Arg = va_arg(Args, char*); + while (Arg) { + + Length = strlen(Arg); + Return = CSend(Buffer, Arg, Length); + if (Return != 0) { + va_end(Args); + return Return; + } + Arg = va_arg(Args, char*); + + } + + va_end(Args); + return 0; +} + +void CInit(CSendBuffer *Buffer, char *Data, unsigned int Size, int (*Callback)(void *, char *, unsigned int), void *CallbackData) +{ + Buffer->Data = Data; + Buffer->Size = Size; + Buffer->Cursor = 0; + + Buffer->Callback = Callback; + Buffer->CallbackData = CallbackData; + + return; +} diff --git a/cbuffer.h b/cbuffer.h new file mode 100644 index 0000000..a85de65 --- /dev/null +++ b/cbuffer.h @@ -0,0 +1,16 @@ +#ifndef CBUFFER_H +#define CBUFFER_H + +typedef struct CSendBuffer { + char *Data; + unsigned int Size, Cursor; + void *CallbackData; + int (*Callback)(void *, char *, unsigned int); +} CSendBuffer; + +int CFlush(CSendBuffer *Buffer); +int CSend(CSendBuffer *Buffer, const char *Data, unsigned int Size); +int CSendStrings(CSendBuffer *Buffer, ...); +void CInit(CSendBuffer *Buffer, char *Data, unsigned int Size, int (*Callback)(void *, char *, unsigned int), void *CallbackData); + +#endif diff --git a/example.c b/example.c new file mode 100644 index 0000000..d9d6e11 --- /dev/null +++ b/example.c @@ -0,0 +1,171 @@ +/* + SSMTP example program. + + On MinGW, use the following to compile; + gcc -Wall example.c ssmtp.c cbuffer.c base64.c -lws2_32 -lDnsapi -o example + + Simple SMTP Mailer. + Copyright (C) 2013 Richard Walmsley + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include +#include + +#include "ssmtp.h" + +#define SMTP_SERVER "localhost" +#define HELO_HOSTNAME SMTP_SERVER + +#define ADDRESS_SENDER "\"Mrs. From\" " +#define ADDRESS_RECEIVER "\"Mr. To\" " + +#define SUBJECT_LINE "SSMTP Testing E-mail" +#define MESSAGE_BODY "This is an example e-mail from an example program with its source code attached." + +#define ATTACHMENT_FILE "example.c" + +static SMTPConn SMTPConnection; +static SMTPAttach Attachment; + +static FILE *AttachmentFile = NULL; + +/* Attachment functions. See below. */ +static int ReadAttach(FILE **File, void *Buffer, unsigned int BufferSize); +static void CloseAttach(FILE **File); + +int main(int argc, char *argv[]) +{ + WSADATA WinsockData; + + if (WSAStartup(MAKEWORD(2,2), &WinsockData) != 0) { + fprintf(stderr, "Couldn't initialize Winsock.\n"); + return 1; + } + + /* + Adjust the state to the inital disconnected state. + This needs to be done as a invalid state will prevent SMTPConnect() from running. + */ + SMTPConnection.State = 0; + + /* Attachment infomation. */ + + /* + This is the filename string that's placed in the e-mail. + It's optional. It's up to the receiver to choose the filename. + In this case, we'll leave it as the actual filename. + */ + Attachment.Filename = ATTACHMENT_FILE; + /* + This is also optional too. If NULL, SSMTP will use 'application/octet-stream'. + */ + Attachment.MIMEType = "text/x-c"; + /* + This data is passed to the read and close functions below. + SSMTP makes no use it so it can be NULL, although you would want something to track the data. + For this simple example, we'll just place the FILE pointer. + */ + Attachment.ReadData = &AttachmentFile; + + /* + These two following function pointers do and the actual reading & closing. + Reading may be called several times. If an internal error occurs, Close is then called. + */ + Attachment.Read = (int (*)(void *, void *, unsigned int))ReadAttach; + Attachment.Close = (void (*)(void *))CloseAttach; + + /* This is a linked list for multiple attachments. */ + Attachment.Next = NULL; + + /* Connecting. */ + if (SMTPConnect(&SMTPConnection, SMTP_SERVER, HELO_HOSTNAME) != SMTP_ERR_SUCCESS) { + fprintf(stderr, "Unable to connect to a working mail server for '%s'.\n", SMTP_SERVER); + WSACleanup(); + return 1; + } + + /* Sender's address. Must come before the receiving addresses. */ + if (SMTPAddress(&SMTPConnection, SMTP_ADDRESS_FROM, ADDRESS_SENDER) != SMTP_ERR_SUCCESS) { + fprintf(stderr, "Unable to send from '%s'.\n", ADDRESS_SENDER); + SMTPDisconnect(&SMTPConnection); + WSACleanup(); + return 1; + } + + /* Receiver's address. SMTP_ADDRESS_TO could also be SMTP_ADDRESS_CC or SMTP_ADDRESS_BCC. */ + if (SMTPAddress(&SMTPConnection, SMTP_ADDRESS_TO, ADDRESS_RECEIVER) != SMTP_ERR_SUCCESS) { + fprintf(stderr, "Unable to send to '%s'.\n", ADDRESS_RECEIVER); + SMTPDisconnect(&SMTPConnection); + WSACleanup(); + return 1; + } + + /* This does the actual sending. */ + if (SMTPData(&SMTPConnection, SUBJECT_LINE, MESSAGE_BODY, &Attachment) != SMTP_ERR_SUCCESS) { + fprintf(stderr, "Unable to relay the e-mail.\n"); + SMTPDisconnect(&SMTPConnection); + WSACleanup(); + return 1; + } + + SMTPDisconnect(&SMTPConnection); + WSACleanup(); + + return 0; +} + +/* + This function is passed a buffer which is to be filled by the function. + It returns the amount within it. + Returning 0 represents EOF and -1 represents an error. + When an error occurs, this function should handle clean up. +*/ +static int ReadAttach(FILE **File, void *Buffer, unsigned int BufferSize) +{ + size_t Result; + + if (*File == NULL) { + *File = fopen(ATTACHMENT_FILE, "rb"); + if (!*File) { + fprintf(stderr, "Error while reading '%s'.\n", ATTACHMENT_FILE); + return -1; + } + } + + Result = fread(Buffer, 1, BufferSize, *File); + + if (Result != BufferSize && ferror(*File)) { + CloseAttach(File); + return -1; + } + + return Result; +} + +/* + Only is called when an internal error occurs. Usually if the connection is lost. +*/ +static void CloseAttach(FILE **File) +{ + fclose(*File); + *File = NULL; + return; +} diff --git a/ssmtp.c b/ssmtp.c new file mode 100644 index 0000000..3058b35 --- /dev/null +++ b/ssmtp.c @@ -0,0 +1,807 @@ +/* + Simple SMTP Mailer. + Copyright (C) 2013 Richard Walmsley + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#ifdef _WIN32 + #define WIN32_MEAN_AND_LEAN + #define _WIN32_WINNT 0x0501 + #include + #include +#endif + +#include + +/* Enables 64-bit time functions. */ +#ifdef _WIN32 + #if __MSVCRT_VERSION__ <= 0x0601 + #undef __MSVCRT_VERSION__ + #define __MSVCRT_VERSION__ 0x0601 + #endif +#endif + +#include +#include /* For the MIME boundary. */ + +/* + Using MinGW's snprintf greatly increases the size of the executable so we don't make use of it. + They return different return codes though. +*/ +#ifdef _WIN32 + #include + #define __snprintf _snprintf +#else + #define __snprintf snprintf +#endif + +#include "ssmtp.h" +#include "cbuffer.h" +#include "base64.h" + +static const char EndOfLine[] = "\r\n"; +static const char EndOfData[] = "\r\n.\r\n"; + +/* Primary used to free the address buffer. */ +static int Shutdown(SMTPConn *Conn) +{ + if (Conn->AddressBuffer != NULL) + free(Conn->AddressBuffer); + + closesocket(Conn->Socket); + Conn->State = SMTP_DISCONNECTED; + + return 0; +} + +static int SendCommand(SMTPConn *Conn, const char *Data, int Size) +{ + unsigned int Offset = 0; + int Return; + + while (Offset < Size) { + + Return = send(Conn->Socket, Data + Offset, Size - Offset, 0); + if (Return == SOCKET_ERROR) { + Shutdown(Conn); + return -1; + } + + Conn->TotalSent += Return; + Offset += Return; + } + + return 0; +} + +static int ReadReply(SMTPConn *Conn, char *Reply, int ReplySize) +{ + int Return, Done, Loop, Char, LineSize, Multiline; + char Buffer[SMTP_BUFFER_SIZE]; + + Done = Char = LineSize = Multiline = 0; + + while (!Done) { + + Return = recv(Conn->Socket, Buffer, sizeof(Buffer), 0); + switch (Return) { + case 0: + case SOCKET_ERROR: + goto Err; + } + + Conn->TotalRecv += Return; + + /* Loop through the bytes recevied. */ + for (Loop = 0; Loop < Return; Loop++) { + + /* Copy what we can into the passed buffer. */ + if (ReplySize > 1) { + *Reply = Buffer[Loop]; + Reply++; + ReplySize--; + } + + /* Ensure the first three bytes of a line are digits as per the spec. */ + if (LineSize < 3) { + if (Buffer[Loop] < '0' || Buffer[Loop] > '9') + goto Err; + } + /* Check for a hypen as the multi-line indicator. */ + else if (LineSize == 3) { + if (Buffer[Loop] == '-') + Multiline = 1; + } + + LineSize++; + + /* Check for the end of line. */ + if (Buffer[Loop] == EndOfLine[Char]) { + Char++; + if (Char >= sizeof(EndOfLine) - 1) { + /* End of line located. */ + + Char = LineSize = 0; + + if (!Multiline) { + Done = 1; + break; + } + + Multiline = 0; + } + } + else + Char = 0; + } + + } + + *Reply = '\0'; + return 0; + + Err: + *Reply = '\0'; + Shutdown(Conn); + return -1; + +} + +/* On most systems, strftime() is easier but the %z specifier isn't standardized and deals with the locale. */ +static int GenerateDate(SMTPConn *Conn, CSendBuffer *CBuffer) +{ + #ifndef _WIN32 + time_t RawTime; + #else + __time64_t RawTime; + #endif + struct tm GMT, Local, *TimeInfo; + char Buffer[38]; /* Can store the largest size possible including the NULL byte. Would require a ten digit year! */ + int Return; + + const char *Days[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; + const char *Months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + + RawTime = + #ifndef _WIN32 + time(NULL); + #else + _time64(NULL); + #endif + if (RawTime == -1) + return 1; + + TimeInfo = + #ifndef _WIN32 + gmtime(&RawTime); + #else + (struct tm *)_gmtime64(&RawTime); + #endif + GMT = *TimeInfo; + + TimeInfo = + #ifndef _WIN32 + localtime(&RawTime); + #else + (struct tm *)_localtime64(&RawTime); + #endif + Local = *TimeInfo; + + /* I'm not entirely sure this works correctly for all timezones. */ + if (Local.tm_isdst > 0) { + GMT.tm_hour += 2; + if (Local.tm_hour > 12) + GMT.tm_hour += 24; + } else if (Local.tm_hour >= 12) + GMT.tm_hour += 24; + + Return = __snprintf(Buffer, sizeof(Buffer), + "%s, %.2d %s %d %.2d:%.2d:%.2d %+03i00", + Days[Local.tm_wday], Local.tm_mday, Months[Local.tm_mon], Local.tm_year + 1900, + Local.tm_hour, Local.tm_min, Local.tm_sec, + GMT.tm_hour - Local.tm_hour); + #ifdef _WIN32 + if (Return <= 0) + #else + if (Return >= sizeof(Buffer) || Return <= 0) + #endif + return 1; + + if (CSendStrings(CBuffer, "Date: ", Buffer, EndOfLine, NULL) != 0) + return -1; + + return 0; +} + +static int MIMEData(CSendBuffer *CBuffer, const char *Body, SMTPAttach *Attachments) +{ + char BoundaryString[64] = "Boundary"; + + char *Char; + unsigned int Var; + + unsigned char DataBuffer[SMTP_BUFFER_SIZE]; + char Base64Buffer[SMTP_BUFFER_SIZE]; + + int Return, Done; + B64Stream B64S; + unsigned int PrintAmount; + + /* Generate a boundary string and check that it's not in the body. */ + + srand(time(NULL)); + Char = &BoundaryString[strlen(BoundaryString)]; + + for (;;) { + + for (Var = 0; Var < SMTP_BOUNDARY_RAND_LENGTH; Var++) + Char[Var] = rand() % 10 + '0'; + Char[Var] = '\0'; + + if (strstr(Body, BoundaryString) == NULL) + break; + + } + + /* Headers. */ + + if (CSendStrings(CBuffer, + "MIME-Version: 1.0", EndOfLine, + "Content-Type: multipart/mixed; boundary=", BoundaryString, EndOfLine, + EndOfLine, + NULL) != 0) + return SMTP_ERR_PROTOCOL; + + /* Start with the body. */ + + if (CSendStrings(CBuffer, "--", BoundaryString, EndOfLine, + "Content-Type: text/plain", EndOfLine, + EndOfLine, + Body, EndOfLine, + NULL) != 0) + return SMTP_ERR_PROTOCOL; + + /* Primary loop for the files. */ + + while (Attachments != NULL) { + + /* File header. */ + if (CSendStrings(CBuffer, "--", BoundaryString, EndOfLine, + "Content-Type: ", (Attachments->MIMEType ? Attachments->MIMEType : "application/octet-stream"), EndOfLine, + "Content-Disposition: attachment", + NULL) != 0) + return SMTP_ERR_PROTOCOL; + + if (Attachments->Filename != NULL) { + if (CSendStrings(CBuffer, "; filename=", Attachments->Filename, + NULL) != 0) + return SMTP_ERR_PROTOCOL; + } + + if (CSendStrings(CBuffer, + EndOfLine, + "Content-Transfer-Encoding: base64", + EndOfLine, + EndOfLine, + NULL) != 0) + return SMTP_ERR_PROTOCOL; + + InitEncode64(&B64S); + Done = Var = 0; + + /* Base64 data. */ + while (!Done) { + + Return = Attachments->Read(Attachments->ReadData, DataBuffer, sizeof(DataBuffer)); + switch (Return) { + case -1: + return SMTP_ERR_DATA; + case 0: + Done = 1; /* Do one last run to flush. */ + } + + B64S.NextIn = DataBuffer; + B64S.AvailIn = Return; + + while (B64S.AvailIn > 0 || Done) { + + B64S.NextOut = Base64Buffer; + B64S.AvailOut = sizeof(Base64Buffer); + + Encode64(&B64S, Done); + + /* Limit the line's length. */ + Return = sizeof(Base64Buffer) - B64S.AvailOut; + Char = Base64Buffer; + + while (Return > 0) { + + PrintAmount = SMTP_LINE_LENGTH - Var; + if (PrintAmount > Return) + PrintAmount = Return; + + if (CSend(CBuffer, Char, PrintAmount) != 0) { + Attachments->Close(Attachments->ReadData); + return SMTP_ERR_PROTOCOL; + } + + Var += PrintAmount; + if (Var >= SMTP_LINE_LENGTH) { + + if (CSend(CBuffer, EndOfLine, sizeof(EndOfLine) - 1) != 0) { + Attachments->Close(Attachments->ReadData); + return SMTP_ERR_PROTOCOL; + } + Var = 0; + + } + + Return -= PrintAmount; + Char += PrintAmount; + + } + + if (Done) + break; + } + + } + + if (Var != 0 && CSend(CBuffer, EndOfLine, sizeof(EndOfLine) - 1) != 0) + return SMTP_ERR_PROTOCOL; + + /* Next. */ + Attachments = Attachments->Next; + } + + /* End. */ + if (CSendStrings(CBuffer, "--", BoundaryString, "--", + NULL) != 0) + return SMTP_ERR_PROTOCOL; + + return 0; +} + +int SMTPData(SMTPConn *Conn, const char *Subject, const char *Body, SMTPAttach *Attachments) +{ + char Buffer[SMTP_BUFFER_SIZE] = "DATA\r\n"; + CSendBuffer CBuffer; + int AddressType, Var; + unsigned int Offset; + + if (Conn->State != SMTP_READY) + return SMTP_ERR_INVALID_STATE; + + if (strstr(Body, EndOfData) != NULL) + return SMTP_ERR_DATA; + + if (SendCommand(Conn, Buffer, strlen(Buffer)) != 0 || + ReadReply(Conn, Buffer, sizeof(Buffer)) != 0) + return SMTP_ERR_PROTOCOL; + + if (atoi(Buffer) != 354) + return SMTP_ERR_FAILURE; + + /* Set up the cached buffer. */ + CInit(&CBuffer, Buffer, sizeof(Buffer), (int (*)(void *, char *, unsigned int))SendCommand, Conn); + + /* Ready to send, so generate the headers. */ + + /* Date. */ + GenerateDate(Conn, &CBuffer); + + /* Addresses. */ + AddressType = -1; + Offset = 0; + + while (Offset < Conn->AddressBufferCursor) { + + if (Conn->AddressBuffer[Offset] != AddressType) { + + if (AddressType != -1) { + if (CSend(&CBuffer, EndOfLine, sizeof(EndOfLine) - 1) != 0) + return SMTP_ERR_PROTOCOL; + } + + AddressType = Conn->AddressBuffer[Offset]; + switch (AddressType) { + case SMTP_ADDRESS_FROM: + Var = CSendStrings(&CBuffer, "From: ", NULL); + break; + case SMTP_ADDRESS_TO: + Var = CSendStrings(&CBuffer, "To: ", NULL); + break; + case SMTP_ADDRESS_CC: + Var = CSendStrings(&CBuffer, "Cc: ", NULL); + break; + default: + return SMTP_ERR_BUFFER; + } + + } + else + Var = CSendStrings(&CBuffer, ",", EndOfLine, " ", NULL); + + if (Var != 0) + return SMTP_ERR_PROTOCOL; + + Var = strlen(&Conn->AddressBuffer[Offset + 1]); + if (CSend(&CBuffer, &Conn->AddressBuffer[Offset + 1], Var) != 0) + return SMTP_ERR_PROTOCOL; + Offset += Var + 2; + } + + if (CSend(&CBuffer, EndOfLine, sizeof(EndOfLine) - 1) != 0) + return SMTP_ERR_PROTOCOL; + + /* Subject line if one was provided. */ + if (Subject) { + if (CSendStrings(&CBuffer, "Subject: ", Subject, EndOfLine, NULL) != 0) + return SMTP_ERR_PROTOCOL; + } + + if (!Attachments) { + /* Main body. */ + if (CSendStrings(&CBuffer, EndOfLine, Body, NULL) != 0) + return SMTP_ERR_PROTOCOL; + } + else + { + Var = MIMEData(&CBuffer, Body, Attachments); + if (Var != 0) + return Var; + } + + /* End data. */ + if (CSendStrings(&CBuffer, EndOfData, NULL) != 0) + return SMTP_ERR_PROTOCOL; + + /* Flush the buffer. */ + if (CFlush(&CBuffer) != 0 || + ReadReply(Conn, Buffer, sizeof(Buffer)) != 0) + return SMTP_ERR_PROTOCOL; + + if (atoi(Buffer) != 250) + return SMTP_ERR_FAILURE; + + return SMTP_ERR_SUCCESS; +} + +int SMTPAddress(SMTPConn *Conn, int Type, const char *Address) +{ + size_t Length; + unsigned int Loop; + + int InQuotes = 0, ReachedEnd = 0; + const char *AddressStart; + unsigned int AddressLength; + + char Buffer[SMTP_BUFFER_SIZE]; + int Return; + + unsigned int Size, AllocSize; + char *AllocBuffer; + + if (Conn->State == SMTP_DISCONNECTED || + (Conn->State == SMTP_CONNECTED && Type != SMTP_ADDRESS_FROM) || + (Conn->State >= SMTP_AWAITING_RECIPIENT && Type == SMTP_ADDRESS_FROM)) + return SMTP_ERR_INVALID_STATE; + + /* Locate the e-mail address incase the string passed contains a name as well. */ + Length = strlen(Address); + + AddressStart = NULL; + AddressLength = 0; + + for (Loop = 0; Loop < Length; Loop++) { + + if (AddressStart) + AddressLength++; + + if (!(InQuotes & 1)) { + + switch (Address[Loop]) { + case '<': + if (AddressStart) + return SMTP_ERR_DATA; + AddressStart = &Address[Loop] + 1; + break; + case '>': + if (!AddressStart) + return SMTP_ERR_DATA; + Loop = Length; /* Break out the loop. */ + AddressLength--; + ReachedEnd = 1; + break; + } + + } + + if (Address[Loop] == '"') + InQuotes++; + } + + /* If there was no brackets found, we'll use the entire string passed. */ + if (!AddressStart) { + AddressStart = Address; + AddressLength = Length; + } + else if (!ReachedEnd) + return SMTP_ERR_DATA; + + if (!memchr(AddressStart, '@', AddressLength)) + return SMTP_ERR_DATA; + + /* If we're here, we may have a valid e-mail address. */ + + if (Type == SMTP_ADDRESS_FROM) + Return = __snprintf(Buffer, sizeof(Buffer), "MAIL FROM:<%.*s>\r\n", AddressLength, AddressStart); + else + Return = __snprintf(Buffer, sizeof(Buffer), "RCPT TO:<%.*s>\r\n", AddressLength, AddressStart); + #ifdef _WIN32 + if (Return <= 0) + #else + if (Return >= sizeof(Buffer) || Return <= 0) + #endif + return SMTP_ERR_BUFFER; + + if (SendCommand(Conn, Buffer, Return) != 0 || + ReadReply(Conn, Buffer, sizeof(Buffer)) != 0) + return SMTP_ERR_PROTOCOL; + + Return = atoi(Buffer); + if (Return != 250 && Return != 251) + return SMTP_ERR_FAILURE; + + /* If not a BCC address, add it to the address buffer. */ + if (Type != SMTP_ADDRESS_BCC) { + + Size = Length + 2; /* Two extra bytes are added for the end and type bytes. */ + if (Size + Conn->AddressBufferCursor > Conn->AddressBufferSize) { + + AllocSize = Size; + if (AllocSize < SMTP_BUFFER_SIZE) + AllocSize = SMTP_BUFFER_SIZE; + Conn->AddressBufferSize += AllocSize; + + AllocBuffer = realloc(Conn->AddressBuffer, Conn->AddressBufferSize); + if (!AllocBuffer) + return SMTP_ERR_BUFFER; + Conn->AddressBuffer = AllocBuffer; + + } + + Conn->AddressBuffer[Conn->AddressBufferCursor] = Type; + memcpy(&Conn->AddressBuffer[Conn->AddressBufferCursor + 1], Address, Length); + Conn->AddressBuffer[Conn->AddressBufferCursor + Size - 1] = '\0'; + Conn->AddressBufferCursor += Size; + } + + if (Conn->State < SMTP_READY) + Conn->State++; + + return SMTP_ERR_SUCCESS; +} + +static int Connect(SMTPConn *Conn, const char *Server, const char *HeloLine) +{ + struct addrinfo Hints, *Results, *Next; + char Buffer[SMTP_BUFFER_SIZE]; + int Return; + #ifdef _WIN32 + CONST DWORD TimeoutLength = SMTP_BLOCKING_TIME; + #endif + + memset(&Hints, 0, sizeof(Hints)); + Hints.ai_family = AF_UNSPEC; + Hints.ai_socktype = SOCK_STREAM; + + if (getaddrinfo(Server, SMTP_DEFAULT_PORT, &Hints, &Results) != 0) + return -1; + + for (Next = Results; Next != NULL; Next = Next->ai_next) { + + Conn->Socket = socket(Next->ai_family, Next->ai_socktype, Next->ai_protocol); + if (Conn->Socket == INVALID_SOCKET) + continue; + + #ifdef _WIN32 + setsockopt(Conn->Socket, SOL_SOCKET, SO_RCVTIMEO, (const char *)&TimeoutLength, sizeof(DWORD)); + #endif + + if (connect(Conn->Socket, Next->ai_addr, Next->ai_addrlen) != 0) { + closesocket(Conn->Socket); + Conn->Socket = INVALID_SOCKET; + continue; + } + + /* Read the header to ensure that it's working. */ + if (ReadReply(Conn, Buffer, sizeof(Buffer)) != 0) { + Conn->Socket = INVALID_SOCKET; + continue; + } + if (atoi(Buffer) != 220) { + closesocket(Conn->Socket); + Conn->Socket = INVALID_SOCKET; + continue; + } + + /* Send our HELO string. */ + Return = __snprintf(Buffer, sizeof(Buffer), "HELO %s\r\n", HeloLine); + #ifdef _WIN32 + if (Return <= 0) + #else + if (Return >= sizeof(Buffer) || Return <= 0) + #endif + { + closesocket(Conn->Socket); + Conn->Socket = INVALID_SOCKET; + continue; + } + if (SendCommand(Conn, Buffer, Return) != 0) { + Conn->Socket = INVALID_SOCKET; + continue; + } + + /* Check its reply. */ + if (ReadReply(Conn, Buffer, sizeof(Buffer)) != 0) { + Conn->Socket = INVALID_SOCKET; + continue; + } + if (atoi(Buffer) != 250) { + closesocket(Conn->Socket); + Conn->Socket = INVALID_SOCKET; + continue; + } + + break; + } + + freeaddrinfo(Results); + + if (Conn->Socket == INVALID_SOCKET) + return -2; + + Conn->State = SMTP_CONNECTED; + + return 0; +} + +/* NOTE: Win32 code. Not portable. */ +#ifdef _WIN32 +static int CompareMXRecord(const void *First, const void *Second) +{ + return (*(DNS_RECORD **)First)->Data.MX.wPreference - (*(DNS_RECORD **)Second)->Data.MX.wPreference; +} + +static int ConnectToMXServer(SMTPConn *Conn, const char *Domain, const char *HeloLine) +{ + DNS_RECORD *DNSResults, *DNSNext, **DNSMXSortArrary; + HANDLE ProcessHeap; + unsigned int Amount, Loop; + + if (DnsQuery_A(Domain, DNS_TYPE_MX, DNS_QUERY_STANDARD, NULL, &DNSResults, NULL) == NOERROR) { + + /* Count the number we have and create an array for them. */ + Amount = 0; + for (DNSNext = DNSResults; DNSNext != NULL; DNSNext = DNSNext->pNext) { + if (DNSNext->wType == DNS_TYPE_MX) + Amount++; + } + + ProcessHeap = GetProcessHeap(); + if (!ProcessHeap) { + DnsRecordListFree(DNSResults, DnsFreeRecordList); + return -2; + } + + DNSMXSortArrary = HeapAlloc(ProcessHeap, 0, Amount * sizeof(DNS_RECORD*)); + if (!DNSMXSortArrary) { + DnsRecordListFree(DNSResults, DnsFreeRecordList); + return -2; + } + + Amount = 0; + for (DNSNext = DNSResults; DNSNext != NULL; DNSNext = DNSNext->pNext) { + if (DNSNext->wType == DNS_TYPE_MX) { + DNSMXSortArrary[Amount] = DNSNext; + Amount++; + } + } + + qsort(&DNSMXSortArrary[0], Amount, sizeof(DNS_RECORD*), CompareMXRecord); + + for (Loop = 0; Loop < Amount; Loop++) { + if (Connect(Conn, DNSMXSortArrary[Loop]->Data.MX.pNameExchange, HeloLine) == 0) + break; + } + + HeapFree(ProcessHeap, 0, DNSMXSortArrary); + + DnsRecordListFree(DNSResults, DnsFreeRecordList); + + } + + /* As per a spec, in a last attempt, try to connect to the A record. */ + if (Conn->State == SMTP_DISCONNECTED) { + if (Connect(Conn, Domain, HeloLine) != 0) + return -1; + } + + return 0; +} +#endif + +int SMTPDisconnect(SMTPConn *Conn) +{ + char Buffer[SMTP_BUFFER_SIZE] = "QUIT\r\n"; + + if (Conn->State == SMTP_DISCONNECTED) + return SMTP_ERR_INVALID_STATE; + + if (SendCommand(Conn, Buffer, strlen(Buffer)) != 0) + return SMTP_ERR_PROTOCOL; + + shutdown(Conn->Socket, SD_SEND); + + /* As per RFC2821, it's recommended that we read the reply. */ + if (ReadReply(Conn, Buffer, sizeof(Buffer)) != 0) + return SMTP_ERR_PROTOCOL; + + Shutdown(Conn); + + return SMTP_ERR_SUCCESS; +} + +int SMTPReset(SMTPConn *Conn) +{ + char Buffer[SMTP_BUFFER_SIZE] = "RSET\r\n"; + + if (Conn->State <= SMTP_CONNECTED) + return SMTP_ERR_INVALID_STATE; + + if (SendCommand(Conn, Buffer, strlen(Buffer)) != 0 || + ReadReply(Conn, Buffer, sizeof(Buffer)) != 0) + return SMTP_ERR_PROTOCOL; + + if (atoi(Buffer) != 250) + return SMTP_ERR_FAILURE; + + Conn->AddressBufferCursor = 0; + Conn->State = SMTP_CONNECTED; + + return SMTP_ERR_SUCCESS; +} + +int SMTPConnect(SMTPConn *Conn, const char *Domain, const char *HeloLine) +{ + if (Conn->State != SMTP_DISCONNECTED) + return SMTP_ERR_INVALID_STATE; + + Conn->TotalRecv = Conn->TotalSent = 0; + + if (ConnectToMXServer(Conn, Domain, HeloLine) != 0) + return SMTP_ERR_FAILURE; + + Conn->AddressBufferSize = Conn->AddressBufferCursor = 0; + Conn->AddressBuffer = NULL; + + return SMTP_ERR_SUCCESS; +} diff --git a/ssmtp.h b/ssmtp.h new file mode 100644 index 0000000..30904c8 --- /dev/null +++ b/ssmtp.h @@ -0,0 +1,68 @@ +#ifndef SMTP_H +#define SMTP_H + +#define SMTP_DEFAULT_PORT "25" +#ifndef SMTP_BUFFER_SIZE + #define SMTP_BUFFER_SIZE 2048 +#endif + +#ifndef SMTP_BLOCKING_TIME + #define SMTP_BLOCKING_TIME 15000 +#endif + +/* The follow is only used for MIME data. */ +#define SMTP_BOUNDARY_RAND_LENGTH 5 +#define SMTP_LINE_LENGTH 76 + +typedef struct SMTPConn { + int Socket; + unsigned int State; + + unsigned int AddressBufferSize, AddressBufferCursor; + char *AddressBuffer; + + unsigned int TotalSent; + unsigned int TotalRecv; +} SMTPConn; + +typedef struct SMTPAttach { + char *Filename; + char *MIMEType; + + void *ReadData; + int (*Read)(void *, void *, unsigned int); + void (*Close)(void *); + + struct SMTPAttach *Next; +} SMTPAttach; + +int SMTPConnect(SMTPConn *Conn, const char *Domain, const char *HeloLine); +int SMTPAddress(SMTPConn *Conn, int Type, const char *Address); +int SMTPData(SMTPConn *Conn, const char *Subject, const char *Body, SMTPAttach *Attachments); +int SMTPReset(SMTPConn *Conn); +int SMTPDisconnect(SMTPConn *Conn); + +enum SMTPStates { + SMTP_DISCONNECTED, + SMTP_CONNECTED, + SMTP_AWAITING_RECIPIENT, + SMTP_READY +}; + +enum SMTPAddressType { + SMTP_ADDRESS_FROM, + SMTP_ADDRESS_TO, + SMTP_ADDRESS_CC, + SMTP_ADDRESS_BCC +}; + +enum SMTPErrors { + SMTP_ERR_INVALID_STATE = -1, + SMTP_ERR_SUCCESS, + SMTP_ERR_FAILURE, + SMTP_ERR_BUFFER, /* Out of memory or static buffer too small. */ + SMTP_ERR_PROTOCOL, /* Protocol error. Server not following the spec or transfer error. */ + SMTP_ERR_DATA /* The data passed to the function is invalid. */ +}; + +#endif