# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""
   Windows gather to collect mail server info
"""

# To reliably function when invoked interactively as "python ./gather_cpu.py",
# we may have to put ../.. on sys.path.
if __name__ == "__main__":
    import os
    import sys
    try:
        import sigma
    except ImportError:
        sys.path.append(os.path.join(os.pardir, os.pardir))
    del sys
    del os

import os
import re
import platform
from win32com.client import GetObject
import socket
import winreg as _winreg
import logging

from sigma.util import console

logger = logging.getLogger("gather.gather_winmailserver")

# _ipre matches ip addresses.
_ipre = r"\d+\.\d+\.\d+\.\d+"
assert re.match(_ipre, "192.168.1.6"), "_ipre is broken"

# _optipre matches an IP address or "*".
_optipre = r"(" + _ipre + r"|\*)"
assert re.match(_optipre, "192.168.1.6"), "_optipre (normal case) is broken."
assert re.match(_optipre, "*"), "_optipre (optional case) is broken."

# Match a port number or "*".
_optport = r"(\d+|\*)"
assert re.match(_optport, "123"), "_optport (normal case) is broken."
assert re.match(_optport, "*"), "_optport (optional case) is broken."

windows_linere = re.compile(
    r"\s*(?P<proto>(TCP|UDP))\s+"
    + r"(?P<localip>" + _ipre + r"):(?P<localport>\d+)\s+"
    + r"(?P<foreignip>" + _optipre + r"):(?P<foreignport>" + _optport+r")\s+"
    + r"(?P<state>[^\s]*)\s+(?P<pid>\d+)\s*")

columns = ["proto", "localip", "localport",
           "foreignip", "foreignport", "state", "pid"]

# this fn gets the profile registry keys for outlook for current user
def GetProfileKeys():
    profileList = []
    keyname = "Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows Messaging Subsystem\\Profiles"
    try:
        root = _winreg.ConnectRegistry(None, _winreg.HKEY_USERS)
        try:
            nsubkeys, nvalues, mtime = _winreg.QueryInfoKey(root)
            for i in range(nsubkeys):
                try:
                    userkeyname = _winreg.EnumKey(root, i)
                except OSError as err:
                    continue 
                # ignore 'local system', 'local service' and 'network service' accounts
                if userkeyname.startswith("S-1-5-18") or userkeyname.startswith("S-1-5-19") or userkeyname.startswith("S-1-5-20"): 
                    continue
                # ignore classes for any user and the default user subkey
                if userkeyname.lower().endswith("_classes") or userkeyname.lower() == ".default": continue
                profileskeyname = "%s\\%s" % (userkeyname, keyname)
                try:
                    profileskey = _winreg.OpenKey(root, profileskeyname)
                except OSError as err:
                    continue 
                try:
                    msubkeys, mvalues, mmtime = _winreg.QueryInfoKey(profileskey)
                    for j in range(msubkeys):
                        akeyname = _winreg.EnumKey(profileskey, j)
                        aprofilekeyname = "%s\\%s\\%s" % (userkeyname, keyname, akeyname)
                        profileList.append(aprofilekeyname)
                finally:
                    _winreg.CloseKey(profileskey)
                
        finally:
            _winreg.CloseKey(root)
    except OSError:
        pass
    return profileList

# this fn tries to read the exchange server name from registry
def GetRegMailServer(table):
    serverList = []
    mailServerList = []
    try:
        profileList = GetProfileKeys()
        root = _winreg.ConnectRegistry(None, _winreg.HKEY_USERS)
        for aProfile in profileList:
            try:
                profileKey = _winreg.OpenKey(root, aProfile)
            except OSError as err:
                continue 
            try:
                msubkeys, mvalues, mmtime = _winreg.QueryInfoKey(profileKey)
                for j in range(msubkeys):
                    akeyname = _winreg.EnumKey(profileKey, j)
                    aguidkeyname = "%s\\%s" % (aProfile, akeyname)
                    try:
                        guidsubkey = _winreg.OpenKey(root, aguidkeyname)
                    except OSError as err:
                        continue
                    try:
                        nsubkeys, nvalues, mtime = _winreg.QueryInfoKey(guidsubkey)
                        for i in range(nvalues):
                            nm, val, typ = _winreg.EnumValue(guidsubkey, i)
                            if not isinstance(nm, str):
                                nm = str(nm)
                            if not isinstance(val, str):
                                val = str(val)
                            if nm.lower() == "001e6602":
                                if val not in serverList:
                                    serverList.append(val)
                                    try:
                                        if not re.match(r"\d+\.\d+\.\d+\.\d+", val):
                                            val = socket.gethostbyname(val)
                                        table.add((val,))
                                        mailServerList.append(val)
                                    except socket.error as msg:
                                        pass
                                    break
                    finally:
                        _winreg.CloseKey(guidsubkey)
            finally:
                _winreg.CloseKey(profileKey)
        _winreg.CloseKey(root)
    except OSError:
        pass

    # get the info from netstat if nothing found in registry
    if len(mailServerList) == 0:
        rel = platform.release().lower()
        if rel != "2000":
            windows_netstat(table)        

# this fn gets the pid for outlook.exe    
def GetOutlookPID():
    pid = ""
    try:
        objItems = GetObject("WinMgMts://./root/cimv2").InstancesOf("Win32_Process")
        for item in objItems:
            caption = item.Properties_("Caption").Value
            if caption.lower() == "outlook.exe":
                pid = item.Properties_("ProcessID").Value
                break
    except:
        logger.exception("winmailserver gather: getting pid for outlook.exe failed")
    return str(pid)

# this fn gets the dns and dc server names
def GetServers():
    serverList = []
    try:
        objItems = GetObject("WinMgMts://./root/cimv2").InstancesOf("Win32_NetworkAdapterConfiguration")
        for item in objItems:
            enabled = item.Properties_("IPEnabled").Value
            if not enabled:
                continue
            dnsList = item.Properties_("DNSServerSearchOrder").Value
            if not dnsList: # It may be None or an empty list
                continue
            for dns in dnsList:
                if not isinstance(dns, str):
                    dns = str(dns)
                serverList.append(dns)
    except:
        logger.exception("winmailserver gather: getting DNS server failed")
    
    try:
        objItems = GetObject("WinMgMts://./root/cimv2").InstancesOf("Win32_NTDomain")
        for item in objItems:
            dc = item.Properties_("DomainControllerAddress").Value
            if dc == "" or dc == None:
                continue
            if dc.startswith("\\"):
                dc = dc[2:]
            #print dc
            serverList.append(dc)
    except:
        logger.exception("winmailserver gather: getting DC info failed")
    
    #print serverList
    return serverList
    
# this fn parses the netstat output for mail server IP
def windows_parse(table, file):
    outlookPID = GetOutlookPID ()
    otherServerList = GetServers()
    mailServerList = []
    
    for line in file:
        match = windows_linere.match(line)
        if match:
            proto, localip, localport, foreignip, foreignport, state, pid = \
                    match.group(*columns)
            if pid == outlookPID:
                if foreignip not in otherServerList:
                    if foreignip == '*' or foreignip == '0.0.0.0':
                        continue
                    if foreignport == '80' or foreignport == '8080' or foreignport == '443':
                        continue
                    if foreignip not in mailServerList:
                        mailServerList.append(foreignip)
                        #print foreignip
                        table.add((foreignip,))
    # get the info from registry if nothing found from netstat info
    #if len(mailServerList) == 0:
    #    GetRegMailServer(table)

def windows_netstat(table):
    file = console.popen("netstat -ano", "r")
    try:
        windows_parse(table, file)
    finally:
        file.close()

def Gather(collector):
    t = collector.table("winmailserver", ["mailserver"])
    if os.name == "nt":
        GetRegMailServer(t)
        

def _test():
    from sigma.gather import gathering
    gathering.test(Gather)

if __name__ == "__main__":
    _test()
