text: /*
   * Copyright 2010 Boris Byk.
   *
   * Licensed under the Apache License, Version 2.0 (the "License"); you may
   * not use this file except in compliance with the License. You may obtain
   * a copy of the License at
   *
   *  http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   * License for the specific language governing permissions and limitations
   * under the License.
   */
  
  #include "StdAfx.h"
  #include "Session.h"
  
  #pragma warning(disable:4482)
  
  namespace DevSmtp
  {
  	Session::Session( Server* r_pServer ) : m_pServer(r_pServer), 
  		m_eState(WAIT_ACCEPT), m_eOpState(NONE), m_hFile(0), m_pFo(this)
  	{
  		ZeroMemory( m_charBuf, DEFAULT_READ_BUFFER_SIZE );
  		m_line.reserve( DEFAULT_READ_BUFFER_SIZE );	
  		IssueAccept();
  	}
  
  	Session::~Session(void)
  	{
  	}
  
  	void Session::IssueAccept(void)
  	{
  		Internal = 0;
  		InternalHigh = 0;
  		Offset = 0;
  		OffsetHigh = 0;
  		hEvent = 0;
  		m_eState = WAIT_ACCEPT;
  
  		ZeroMemory( m_addrBlock, ACCEPT_ADDRESS_LENGTH * 2 );
  		m_hSocket = WSASocket( PF_INET, SOCK_STREAM, IPPROTO_TCP, 0, 0, 
  			WSA_FLAG_OVERLAPPED );
  		assert(m_hSocket != INVALID_SOCKET);
  
  		// Associate the client socket with the I/O Completion Port.
  		CreateIoCompletionPort( 
  			(HANDLE)m_hSocket,
  			m_pServer->m_hIoPort, 
  			COMPLETION_KEY_IO, 0 );
  
  		BOOL res = AcceptEx( m_pServer->m_hListener, m_hSocket, m_addrBlock, 0, ACCEPT_ADDRESS_LENGTH, 
  			ACCEPT_ADDRESS_LENGTH, NULL, this );
  		int err;
  
  		if (!(res == FALSE && (err = WSAGetLastError()) == ERROR_IO_PENDING))
  		{
  			LOG1(_T("Fail AcceptEx (%d)"), err);
  			exit(2);
  		}
  	}
  
  	void Session::OnIoComplete(DWORD rNumTransferred)
  	{	
  		switch (m_eState)
  		{
  		case WAIT_ACCEPT:
  			CompleteAccept();
  			break;
  		case WAIT_REQUEST:
  			CompleteReadLine( rNumTransferred );
  			break;
  		case WAIT_SEND:
  			CompleteWrite( rNumTransferred );
  			break;
  		case WAIT_REQUEST_DATA:
  			CompleteReadData( rNumTransferred );
  			break;
  		case WAIT_WRITE:
  			CompleteWriteFile( rNumTransferred );
  			break;
  		}
  	}
  
  	void Session::CompleteReadLine(DWORD rNumTransferred)
  	{
  		bool shdIssue = true;
  
  		if (rNumTransferred > 0)
  		{
  			DWORD l_charPos = 0;
  			int span;
  			DWORD char_pos = l_charPos;
  
  			do 
  			{
  				char ch = m_charBuf[char_pos];
  				if (ch == '\r' || ch == '\n')
  				{
  					span = char_pos - l_charPos;
  					m_line.append( &(m_charBuf[l_charPos]), span );
  					l_charPos = char_pos + 1;
  
  					if ((ch == '\r' && l_charPos < rNumTransferred) && (m_charBuf[l_charPos] == '\n'))
  					{
  						l_charPos++;
  					}
  
  					OnReadLine();
  					m_line.clear();
  					return;
  				}
  				else
  				{
  					char_pos++;
  				}
  			} 
  			while (char_pos < rNumTransferred);
  			span = rNumTransferred - l_charPos;
  			m_line.append( &(m_charBuf[l_charPos]), span );
  		}
  
  		if (shdIssue)
  			IssueRead();
  	}
  
  	void Session::CompleteReadData( DWORD rNumTransferred )
  	{
  		DWORD l_char_pos = 0;
  		int span;
  		DWORD char_pos = l_char_pos;
  		bool shdIssue = true;
  		char sep[] = "\r\n.\r\n";
  		int si = 0;
  
  		do 
  		{
  			char ch = m_charBuf[char_pos];
  
  			if (ch == sep[si])
  			{
  				si++;
  			}
  			else if (si > 0)
  			{
  				si = 0;
  
  				if (ch == sep[si])
  				{
  					si++;
  				}
  			}
  
  			if (si == 5) //separator length)
  			{
  				span = char_pos - l_char_pos;
  				m_line.append( &(m_charBuf[l_char_pos]), span - 3 );
  				l_char_pos = char_pos + 1;
  
  				OnReadLine();
  
  				m_line.clear();
  				return;
  			}
  
  			char_pos++;
  		} 
  		while (char_pos < rNumTransferred);
  		span = rNumTransferred - l_char_pos;
  		m_line.append( &(m_charBuf[l_char_pos]), span );
  
  		if (shdIssue)
  			IssueRead(STATE::WAIT_REQUEST_DATA);
  	}
  
  	void Session::CompleteAccept()
  	{
  		setsockopt( m_hSocket, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT,
  			(char*)&m_pServer->m_hListener, sizeof(SOCKET) );
  
  		// fetch connection context and log it
  		SOCKADDR_IN sin;
  		int nameLen = sizeof(sin);
  		getpeername( m_hSocket, (PSOCKADDR)&sin, &nameLen);
  		char* remote_ip = inet_ntoa(sin.sin_addr);
  		char buffer[26];
  		sprintf_s(buffer, sizeof(buffer), "Connected %s", remote_ip);
  		Log(TOLPTCHAR(buffer));
  
  		CompleteOperation();
  	}
  
  	void Session::OnReadLine()
  	{
  		_TCHAR buf[65];
  		_itot_s(m_hSocket, buf, 10);
  		LOG2(_T("C: %s"), buf, TOLPTCHAR(&m_line[0]));
  		
  		CompleteOperation();
  	}
  
  	void Session::IssueRead()
  	{
  		IssueRead(STATE::WAIT_REQUEST);
  	}
  
  	void Session::IssueRead(STATE rState)
  	{
  		m_eState = rState;
  		
  		BOOL res = ReadFile( (HANDLE)m_hSocket, m_charBuf, DEFAULT_READ_BUFFER_SIZE, 
  			NULL, (LPOVERLAPPED)this );
   
  		if (!res)
  		{
  			int err = GetLastError();
  			if (err == ERROR_IO_PENDING)
  			{
  				return;
  			}
  			else if (err == ERROR_NETNAME_DELETED)
  			{
  				IssueReset();
  				return;
  			}
  
  			assert(false);
  		}
  	}
  
  	void Session::IssueWrite(size_t pos)
  	{
  		m_eState = WAIT_SEND;
  		BOOL res = WriteFile( (HANDLE)m_hSocket, &(m_outline[pos]), m_outline.size() - pos, NULL, (LPOVERLAPPED)this );
  		int err;
  
  		if (!(res == TRUE || (err = WSAGetLastError()) == ERROR_IO_PENDING))
  		{
  			LOG1(_T("Fail WriteFile (%d)"), err);
  		}
  	}
  
  	void Session::CompleteWrite( DWORD rNumTransferred )
  	{
  		if (rNumTransferred < m_outline.size())
  		{
  			IssueWrite( rNumTransferred );
  			return;
  		}
  
  		_TCHAR buf[65];
  		_itot_s(m_hSocket, buf, 10);
  		LOG2(_T("S: %s"), buf, TOLPTCHAR(&(m_outline.substr(0, m_outline.size() - 2)[0])));
  		CompleteOperation();
  	}
  
  	void Session::CompleteOperation()
  	{
  		if (m_eState == STATE::WAIT_REQUEST && 
  			strncmp(m_line.c_str(), "QUIT", 4) == 0)
  		{
  			m_eOpState = OPSTATE::RE_QUIT;
  			m_outline = "221 Bye\r\n";
  			IssueWrite(0);
  			return;
  		}
  
  		if (m_eState == STATE::WAIT_REQUEST &&
  			strncmp(m_line.c_str(), "RSET", 4) == 0)
  		{
  			m_eOpState = OPSTATE::RE_DOT;
  			m_outline = "250 Ok\r\n";
  			IssueWrite(0);
  			return;
  		}
  
  		if (m_eState == STATE::WAIT_REQUEST &&
  			strncmp(m_line.c_str(), "NOOP", 4) == 0)
  		{
  			// does not change state
  			m_outline = "250 Ok\r\n";
  			IssueWrite(0);
  			return;
  		}
  
  		switch (m_eOpState)
  		{
  		case OPSTATE::NONE:
  			m_eOpState = OPSTATE::SEND_WELCOME;
  			m_outline = "220 DevSmtp\r\n";
  			IssueWrite(0);
  			break;
  		case OPSTATE::SEND_WELCOME:
  			m_eOpState = OPSTATE::RCV_HELLO;
  			IssueRead();
  			break;
  		case OPSTATE::RCV_HELLO:
  			if (strncmp(m_line.c_str(), "HELO", 4) != 0)
  			{
  				m_eOpState = OPSTATE::SEND_WELCOME;
  				m_outline = "500 Command not recognized: EHLO\r\n";
  				IssueWrite(0);
  				return;
  			}
  
  			m_eOpState = OPSTATE::RE_RCV_HELLO;
  			m_outline = "250 Ok\r\n";
  			IssueWrite(0);
  			break;
  		case OPSTATE::RE_RCV_HELLO:
  			m_eOpState = OPSTATE::MAIL;
  			IssueRead();
  			break;
  		case OPSTATE::MAIL:
  			if (strncmp(m_line.c_str(), "MAIL", 4) != 0)
  			{
  				m_eOpState = OPSTATE::RE_RCV_HELLO;
  				m_outline = "500 Command not recognized: EHLO\r\n";
  				IssueWrite(0);
  				return;
  			}
  			m_eOpState = OPSTATE::RE_MAIL;
  			m_outline = "250 Ok\r\n";
  			IssueWrite(0);
  			break;
  		case OPSTATE::RE_MAIL:
  			m_eOpState = OPSTATE::RCPT;
  			IssueRead();
  			break;
  		case OPSTATE::RCPT:
  			m_eOpState = OPSTATE::RE_RCPT;
  			m_outline = "250 Ok\r\n";
  			IssueWrite(0);
  			break;
  		case OPSTATE::RE_RCPT:
  			m_eOpState = OPSTATE::RCPT_OR_DATA;
  			IssueRead();
  			break;
  		case OPSTATE::RCPT_OR_DATA:
  			if (strncmp(m_line.c_str(), "DATA", 4) == 0)
  			{
  				m_eOpState = OPSTATE::RE_DATA;
  				m_outline = "354 End data with <CR><LF>.<CR><LF>\r\n";
  			}
  			else
  			{			
  				m_eOpState = OPSTATE::RE_RCPT;
  				m_outline = "250 Ok\r\n";
  			}
  			
  			IssueWrite(0);
  			break;
  		case OPSTATE::RE_DATA:
  			m_eOpState = OPSTATE::DOT;
  			IssueRead(STATE::WAIT_REQUEST_DATA);
  			break;
  		case OPSTATE::DOT:
  			m_eOpState = OPSTATE::WRITE_FILE;
  			IssueWriteFile(0);
  			break;
  		case OPSTATE::WRITE_FILE:
  			m_eOpState = OPSTATE::RE_DOT;
  			m_outline = "250 Ok\r\n";
  			IssueWrite(0);
  			break;
  		case OPSTATE::RE_DOT:
  			m_eOpState = OPSTATE::QUIT_OR_MAIL;
  			IssueRead();
  			break;
  		case OPSTATE::QUIT_OR_MAIL:
  			if (strncmp(m_line.c_str(), "MAIL", 4) == 0)
  			{
  				m_eOpState = OPSTATE::RE_MAIL;
  				m_outline = "250 Ok\r\n";
  				IssueWrite(0);
  			}
  			break;
  		case OPSTATE::RE_QUIT:
  			IssueReset();
  			break;
  		}
  	}
  
  	void Session::IssueReset()
  	{
  		Log(_T("Closed socket."));
  		closesocket(m_hSocket);
  		m_eOpState = OPSTATE::NONE;
  		IssueAccept();
  	}
  
  	bool Session::CanProcessZero()
  	{
  		return m_eState == STATE::WAIT_ACCEPT;
  	}
  
  	void Session::IssueWriteFile(size_t pos)
  	{
  		if (m_hFile == 0)
  		{
  			byte attempts = 5;
  			_TCHAR file_name[_MAX_PATH];
  						
  			tstring full_path;
  			full_path.reserve(_MAX_PATH);
  			full_path.append(m_pServer->m_pPath);
  			full_path.append("email-");
  
  			time_t tt = time( NULL );
  			srand((unsigned)tt);
  			_TCHAR tmpbuf[16];
  			struct tm today;
  			localtime_s( &today, &tt );
  			_tcsftime(tmpbuf, sizeof(tmpbuf) / sizeof(_TCHAR), _T("%Y%m%d-%H%M%S"), &today);
  
  			full_path.append(tmpbuf);
  		
  			while (TRUE)
  			{
  				_stprintf_s(file_name, sizeof(file_name) / sizeof(_TCHAR), _T("%s-(%d).eml"), full_path.c_str(), rand());
  
  				LOG1(_T("Saving email to '%s'"), file_name);
  
  				m_hFile = CreateFile(file_name, 
  					GENERIC_WRITE, 
  					0, 
  					NULL, 
  					CREATE_NEW,
  					FILE_FLAG_OVERLAPPED | FILE_FLAG_WRITE_THROUGH,
  					NULL);
  
  				if (m_hFile == INVALID_HANDLE_VALUE)
  				{
  					LOG1(_T("Can't open file to write (%d)."), GetLastError());
  
  					if (--attempts > 0)
  						continue;
  					else
  					{
  						IssueReset();
  						return;
  					}
  				}
  
  				break;
  			}
  
  			HANDLE hcport = CreateIoCompletionPort( 
  				m_hFile,
  				m_pServer->m_hIoPort, 
  				COMPLETION_FILE_KEY_IO, 0 );
  
  			assert(hcport != NULL);
  
  			m_eState = WAIT_WRITE;
  
  			m_pFo.Internal = 0;
  			m_pFo.InternalHigh = 0;
  			m_pFo.Offset = 0;
  			m_pFo.OffsetHigh = 0;
  			m_pFo.hEvent = 0;
  		}
  		else
  		{
  			LOG1(_T("Used existing file handler %d"), m_hFile);
  		}
  		
  		BOOL res = WriteFile( m_hFile, &(m_line[pos]), m_line.size() - pos, NULL, &m_pFo );
  		int err;
  
  		if (!(res == TRUE || (err = GetLastError()) == ERROR_IO_PENDING))
  		{
  			LOG1(_T("Fail WriteFile (%d)"), err);
  			CloseHandle(m_hFile);
  			m_hFile = 0;
  
  			IssueReset();
  		}
  	}
  
  	void Session::CompleteWriteFile( DWORD rNumTransferred )
  	{
  		if (rNumTransferred < m_line.size())
  		{
  			IssueWriteFile( rNumTransferred );
  			return;
  		}
  
  		CloseHandle(m_hFile);
  		m_hFile = 0;
  
  		CompleteOperation();
  	}
  }
  