csmtpwinsocksmtpclient

Winsock based SMTP Client: stuck on recv() after client data is sent


I'm trying to make a simple SMTP client to learn how SMTP actually works, but I'm having some issues that I think have to do with how my code interacts with winsock. All the code that I have done was made thanks to Microsoft's guide on how to Create a basic Winsock Application.

What I've done so far, is try to mimic an SMTP client terminal, in where I can issue the different commands and see what the server answers. When I learn how the exact procedure for different actions are, I will automate the code so that it can easily "send a mail" or "read inbox".

My code as is, lets me receive the server greeting when the connection is opened, and I can try to send a message (like HELO 1.2.3.4), but the code gets stuck at *iResult = recv(*ConnectSocket, out, 200, 0);.

This is the output I get whe I try to use my code:

Bytes received: 111
220 CP6P284CA0098.outlook.office365.com Microsoft ESMTP MAIL Service ready at Tue, 28 Feb 2023
18:17:15 +0000

$> HELO 1.2.3.4
(some time later)
Bytes received: 127
451 4.7.0 Timeout waiting for client input [CP6P284CA0098.BRAP284.PROD.OUTLOOK.COM 2023-02-28T18:22:16.228Z 08DB18B4E5C7B417]

$>

I dont have full knoweldge on how Winsock works, so maybe its a dumb error. I came to code this with some knoweldge on making SMTP clients on arduino, but that's it.

Here is my code so far:

#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#define _WIN32_WINNT 0x0501

#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#include <stdio.h>
#include <stdlib.h>

#define DEFAULT_PORT "25"

char data[500];

int initWinsock(int *, WSADATA *);
int makeSocket(int *, struct addrinfo *, struct addrinfo **, struct addrinfo **, SOCKET *);
int connSocket(int *, SOCKET *, struct addrinfo *, struct addrinfo *);
int clientReceive(int *, SOCKET *, char *);
int clientSend(int *, char *, SOCKET *);
int closeSocketListener(int *, SOCKET *);
int closeSocket(SOCKET *);

int main() {
    SOCKET ConnectSocket = INVALID_SOCKET;
    WSADATA wsaData;
    int iResult;
    struct addrinfo *result = NULL,
                    *ptr = NULL,
                    hints;
    ZeroMemory( &hints, sizeof(hints) );
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    
    initWinsock(&iResult, &wsaData);

    makeSocket(&iResult, &hints, &result, &ptr, &ConnectSocket);

    connSocket(&iResult, &ConnectSocket, ptr, result);

    while (1){
        char prompt[200] = {0};
        clientReceive(&iResult, &ConnectSocket, data);
        printf("%s\n$> ", data);
        fgets(prompt, sizeof prompt, stdin);
        clientSend(&iResult, prompt, &ConnectSocket);
    }
    

    closeSocketListener(&iResult, &ConnectSocket);

    closeSocket(&ConnectSocket);

    system("pause");
    return 0;
}

int initWinsock(int *iResult, WSADATA *wsaData){
    // Initialize Winsock
    *iResult = WSAStartup(MAKEWORD(2,2), wsaData);
    if (*iResult != 0) {
        printf("WSAStartup failed: %d\n", *iResult);
        return 1;
    }
    return 0;
}

int makeSocket(int *iResult, struct addrinfo *hints, struct addrinfo **result, struct addrinfo **ptr, SOCKET *ConnectSocket){
    // Resolve the server address and port
    *iResult = getaddrinfo("52.97.26.134", DEFAULT_PORT, hints, result);
    if (*iResult != 0) {
        printf("getaddrinfo failed: %d\n", *iResult);
        WSACleanup();
        return 1;
    }

    // Attempt to connect to the first address returned by
    // the call to getaddrinfo
    *ptr = *result;

    // Create a SOCKET for connecting to server
    *ConnectSocket = socket((*ptr)->ai_family, (*ptr)->ai_socktype, (*ptr)->ai_protocol);

    if (*ConnectSocket == INVALID_SOCKET) {
        printf("Error at socket(): %d\n", WSAGetLastError());
        freeaddrinfo(*result);
        WSACleanup();
        return 1;
    }
    return 0;
}

int connSocket(int *iResult, SOCKET *ConnectSocket, struct addrinfo *ptr, struct addrinfo *result){
    // Connect to server.
    *iResult = connect( *ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
    if (*iResult == SOCKET_ERROR) {
        closesocket(*ConnectSocket);
        *ConnectSocket = INVALID_SOCKET;
    }

    // Should really try the next address returned by getaddrinfo
    // if the connect call failed
    // But for this simple example we just free the resources
    // returned by getaddrinfo and print an error message

    freeaddrinfo(result);

    if (*ConnectSocket == INVALID_SOCKET) {
        printf("Unable to connect to server!\n");
        WSACleanup();
        return 1;
    }
    return 0;
}

int clientReceive(int *iResult, SOCKET *ConnectSocket, char *out){
    *iResult = recv(*ConnectSocket, out, 200, 0);
    if (iResult > 0) {
        printf("Bytes received: %d\n", *iResult);
        return 0;
    } else if (iResult == 0) {
        printf("Connection closed\n");
        return 1;
    } else {
        printf("recv failed: %d\n", WSAGetLastError());
        return 2;
    }

    return -1;
}

int clientSend(int *iResult, char *sendbuf, SOCKET *ConnectSocket){
    *iResult = send(*ConnectSocket, sendbuf, (int) strlen(sendbuf), 0);
    if (*iResult == SOCKET_ERROR) {
        printf("send failed: %d\n", WSAGetLastError());
        closesocket(*ConnectSocket);
        WSACleanup();
        return 1;
    }
    return 0;
}

int closeSocketListener(int *iResult, SOCKET *ConnectSocket){
    // shutdown the send half of the connection since no more data will be sent
    *iResult = shutdown(*ConnectSocket, SD_SEND);
    if (*iResult == SOCKET_ERROR) {
        printf("shutdown failed: %d\n", WSAGetLastError());
        closesocket(*ConnectSocket);
        WSACleanup();
        return 1;
    }
    return 0;
}

int closeSocket(SOCKET *ConnectSocket){
    // cleanup
    closesocket(*ConnectSocket);
    WSACleanup();
    return 0;
}

Solution

  • outlook.office365.com server expects that each command ends with CRLF as required by SMTP standard and not just LF. This is in contrast to Postfix and some other servers which are fine with just LF. I have quickly checked that sending HELO message with ncat outlook.office365.com 587 does not work and sending HELO message with ncat --crlf outlook.office365.com 587 works.

    fgets stores "\n" (LF) at the end of the buffer, but you need to send "\r\n" (CRLF) after the command.

    You can correct it as follows:

    int clientSend(int *iResult, char *sendbuf, SOCKET *ConnectSocket){
        int l = strlen(sendbuf);
        if (l == 0) return 0;
        if (sendbuf[l - 1] == '\n' && (l == 1 || sendbuf[l - 2] != '\r')) {
            sendbuf[l - 1] = '\r';
            sendbuf[l++] = '\n';
        }    
        *iResult = send(*ConnectSocket, sendbuf, l, 0);
        ...
    }
    

    Note that there is always space available in sendbuf for replacing LF with CRLF because it is guaranteed to end with \0 after fgets(). We do not need this \0 because we specify the length explicitly as an argument to send() and can overwrite it with LF.