1+ using Microsoft . Graph ;
2+ using Azure . Identity ;
3+ using System . Net . Sockets ;
4+ using System . Net ;
5+ using System . Text . RegularExpressions ;
6+ using System . Text ;
7+ using Microsoft . Kiota . Abstractions ;
8+ using Microsoft . Win32 ;
9+ using Microsoft . Extensions . Logging ;
10+
11+ namespace SMTP2Graph ;
12+
13+ class Service
14+ {
15+ private static ILogger < Service > Logger = LoggerFactory . Create ( ( lf ) => {
16+ lf . AddSimpleConsole ( ( sc ) => {
17+ sc . SingleLine = true ;
18+ sc . TimestampFormat = "MM/dd/yyyy: hh:mm:ss: " ;
19+ } ) ;
20+ if ( OperatingSystem . IsWindows ( ) ) lf . AddEventLog ( ) ;
21+ } ) . CreateLogger < Service > ( ) ;
22+
23+ public static void Main ( )
24+ {
25+ Logger . LogInformation ( "Initializing SMTP2Graph..." ) ;
26+ var mail_listener = new TcpListener ( IPAddress . Any , 25 ) ;
27+ var scopes = new [ ] { "https://graph.microsoft.com/.default" } ;
28+
29+ var options = new ClientSecretCredentialOptions
30+ {
31+ AuthorityHost = AzureAuthorityHosts . AzurePublicCloud ,
32+ } ;
33+
34+ Logger . LogInformation ( "Connecting to MS Graph..." ) ;
35+
36+ GraphServiceClient graphClient = null ;
37+
38+ try
39+ {
40+ var clientSecretCredential = new ClientSecretCredential (
41+ GetConfigOption ( "TENANT_ID" ) ,
42+ GetConfigOption ( "CLIENT_ID" ) ,
43+ GetConfigOption ( "CLIENT_SECRET" ) ,
44+ options
45+ ) ;
46+ graphClient = new GraphServiceClient ( clientSecretCredential , scopes ) ;
47+ }
48+ catch ( Exception e )
49+ {
50+ Logger . LogCritical ( e , "Failed to connect to MS Graph." ) ;
51+ Environment . Exit ( 0 ) ;
52+ }
53+
54+ Logger . LogInformation ( "Starting SMTP listener loop, ready for connections." ) ;
55+ mail_listener . Start ( ) ;
56+ while ( true )
57+ {
58+ var client = mail_listener . AcceptTcpClient ( ) ;
59+ Logger . LogInformation ( "Accepting connection, attempting to process..." ) ;
60+ _ = Task . Run ( ( ) => {
61+ try
62+ {
63+ client . NoDelay = true ;
64+ var stream = client . GetStream ( ) ;
65+ var sw = new StreamWriter ( stream ) ;
66+ var sr = new StreamReader ( stream ) ;
67+ sw . AutoFlush = true ;
68+ sw . Write ( $ "220 { Environment . MachineName } SMTP2Graph Service ready\r \n ") ;
69+ HandleConnection ( client , stream , sw , sr ) ;
70+ }
71+ catch ( Exception e )
72+ {
73+ if ( client . Connected ) client . Close ( ) ;
74+ Logger . LogError ( e , "An error occured in the SMTP listener loop." ) ;
75+ }
76+ } ) ;
77+ }
78+
79+ string GetConfigOption ( string Key )
80+ {
81+ string ? env ;
82+ env = Environment . GetEnvironmentVariable ( Key ) ;
83+ if ( env != null ) return env ;
84+ if ( OperatingSystem . IsWindows ( ) ) env = ( string ? ) ( Registry . LocalMachine . OpenSubKey ( @"SOFTWARE\SMTP2Graph" ) ? . GetValue ( Key ) ) ;
85+ if ( env != null ) return env ;
86+ Logger . LogCritical ( $ "Config option not set: { Key } .") ;
87+ Environment . Exit ( 0 ) ;
88+ return string . Empty ;
89+ }
90+
91+ void HandleConnection ( TcpClient client , Stream stream , StreamWriter sw , StreamReader sr )
92+ {
93+ string From = string . Empty ;
94+ string MIME = string . Empty ;
95+ while ( true )
96+ {
97+ var msg = sr . ReadLine ( ) ;
98+ if ( msg == null ) continue ;
99+ if ( msg . StartsWith ( "HELO" ) || msg . StartsWith ( "EHLO" ) )
100+ {
101+ Logger . LogInformation ( "HELO" ) ;
102+ sw . Write ( "250 OK\r \n " ) ;
103+ continue ;
104+ }
105+ if ( msg . StartsWith ( "MAIL FROM:" ) )
106+ {
107+ Logger . LogInformation ( "MAIL FROM" ) ;
108+ #pragma warning disable SYSLIB1045
109+ From = Regex . Match ( msg , "<(.*)>" ) . Groups [ 1 ] . Value ;
110+ #pragma warning restore SYSLIB1045
111+ sw . Write ( "250 OK\r \n " ) ;
112+ continue ;
113+ }
114+ if ( msg . StartsWith ( "RCPT TO:" ) )
115+ {
116+ Logger . LogInformation ( "RCPT TO" ) ;
117+ sw . Write ( "250 OK\r \n " ) ;
118+ continue ;
119+ }
120+ if ( msg . StartsWith ( "DATA" ) )
121+ {
122+ Logger . LogInformation ( "DATA" ) ;
123+ sw . Write ( "354 Start mail input; end with <CRLF>.<CRLF>\r \n " ) ;
124+ MIME = ReadMIME ( sr ) ;
125+ sw . Write ( "250 OK\r \n " ) ;
126+ Logger . LogInformation ( "Sending to MS Graph..." ) ;
127+ SendMessage ( From , MIME ) ;
128+ Logger . LogInformation ( "Sent." ) ;
129+ continue ;
130+ }
131+ if ( msg . StartsWith ( "QUIT" ) )
132+ {
133+ Logger . LogInformation ( "QUIT" ) ;
134+ sw . Write ( $ "221 { Environment . MachineName } Service closing transmission channel\r \n ") ;
135+ break ;
136+ }
137+ }
138+ Logger . LogInformation ( "Closing connection..." ) ;
139+ client . Close ( ) ;
140+ Logger . LogInformation ( "Done." ) ;
141+ }
142+
143+ string ReadMIME ( StreamReader sr )
144+ {
145+ string MIME = string . Empty ;
146+ while ( true )
147+ {
148+ var msg = sr . ReadLine ( ) ;
149+ if ( msg == "." ) break ;
150+ if ( msg == ".." )
151+ {
152+ msg = "." ;
153+ }
154+ MIME += msg + "\r \n " ;
155+ }
156+ return MIME ;
157+ }
158+
159+ void SendMessage ( string From , string MIME )
160+ {
161+ var mimeb64bytes = Encoding . UTF8 . GetBytes (
162+ Convert . ToBase64String (
163+ Encoding . UTF8 . GetBytes ( MIME )
164+ )
165+ ) ;
166+ var request = new RequestInformation
167+ {
168+ URI = new ( $ "https://graph.microsoft.com/v1.0/users/{ From } /sendMail") ,
169+ HttpMethod = Method . POST
170+ } ;
171+ request . Headers . Add ( "Content-Type" , "text/plain" ) ;
172+ request . Content = new MemoryStream ( mimeb64bytes ) ;
173+ _ = graphClient . RequestAdapter . SendNoContentAsync ( request ) ;
174+ }
175+ }
176+ }
0 commit comments