Benutzer-Werkzeuge

Webseiten-Werkzeuge


microsoft_exchange

Unterschiede

Hier werden die Unterschiede zwischen zwei Versionen angezeigt.

Link zu dieser Vergleichsansicht

Beide Seiten der vorigen Revision Vorhergehende Überarbeitung
Nächste Überarbeitung
Vorhergehende Überarbeitung
microsoft_exchange [2026/01/14 10:26]
jango [Public Folder]
microsoft_exchange [2026/02/27 09:40] (aktuell)
jango [EMS]
Zeile 315: Zeile 315:
  
 ====Logs==== ====Logs====
 +
 +Send und ReceiveLog
 +
 +<code>
 +# Frontend Transport
 +C:\Program Files\Microsoft\Exchange Server\V15\TransportRoles\Logs\FrontEnd\ProtocolLog
 +# Hub Transport
 +C:\Program Files\Microsoft\Exchange Server\V15\TransportRoles\Logs\Hub\ProtocolLog
 +</code>
  
 Aufgaben kann man mit [[coding:powershell#exchange|Powershell]] automatisieren. Aufgaben kann man mit [[coding:powershell#exchange|Powershell]] automatisieren.
Zeile 385: Zeile 394:
 Get-PublicFolder -Recurse -ResultSize Unlimited Get-PublicFolder -Recurse -ResultSize Unlimited
  
-nur mailaktivierte öffentliche Ordner +Nur Top-Level 
-Get-MailPublicFolder -ResultSize unlimited+Get-PublicFolder -ResultSize Unlimited -Recurse | select Name,ParentPath | where-object { $_.ParentPath -eq "\"
 + 
 +Add-PublicFolderClientPermission -Identity \My-Folder -User test.user -AccessRights Editor|Owner|Publisher 
 +Get-PublicFolderClientPermission -Identity \My-Folder 
 +Remove-PublicFolderClientPermission -Identity \My-Folder -User test.user
 </code> </code>
 +
 +<code>
 +RunspaceId                     : f6622ea7-e6d2-4528-97b1-036c9298cf3d
 +Identity                       : \
 +Name                           : IPM_SUBTREE
 +MailEnabled                    : False
 +MailRecipientGuid              :
 +ParentPath                     :
 +LostAndFoundFolderOriginalPath :
 +ContentMailboxName             : PFMailbox1
 +ContentMailboxGuid             : 442dc65a-3bbc-471e-afcd-b0244b273847
 +EformsLocaleId                 :
 +PerUserReadStateEnabled        : True
 +EntryId                        : 000000001A447390AA6611CD9BC800AA002FC45A0300047699F147FA0A4B891FFFB0825125790000000000020000
 +DumpsterEntryId                : 000000001A447390AA6611CD9BC800AA002FC45A0300FF1CBA839DBE6F42B05F589E5BA643340000000000020000
 +ParentFolder                   : 000000001A447390AA6611CD9BC800AA002FC45A0300047699F147FA0A4B891FFFB0825125790000000000010000
 +OrganizationId                 :
 +AgeLimit                       :
 +RetainDeletedItemsFor          :
 +ProhibitPostQuota              : Unlimited
 +IssueWarningQuota              : Unlimited
 +MaxItemSize                    : Unlimited
 +LastMovedTime                  :
 +AdminFolderFlags               :
 +FolderSize                     : 0
 +HasSubfolders                  : True
 +FolderClass                    :
 +FolderPath                     : {}
 +AssociatedDumpsterFolders      :
 +DefaultFolderType              : None
 +ExtendedFolderFlags            : SharedViaExchange
 +MailboxOwnerId                 : domain.local/Users/PFMailbox1
 +IsValid                        : True
 +ObjectState                    : Unchanged
 +</code>
 +
 +bzw.
 +
 +<code>
 +RunspaceId                     : f6622ea7-e6d2-4528-97b1-036c9298cf3d
 +Identity                       : \DUMMY-Newsletter
 +Name                           : DUMMY-Newsletter
 +MailEnabled                    : True
 +MailRecipientGuid              : 2956ec64-1f20-4ee6-a5f2-dbeb2c10b5b1
 +ParentPath                     : \
 +LostAndFoundFolderOriginalPath :
 +ContentMailboxName             : PFMailbox2
 +ContentMailboxGuid             : 70c4457e-a502-4170-bf73-fcb4f17cce2c
 +EformsLocaleId                 :
 +PerUserReadStateEnabled        : True
 +EntryId                        : 000000001A447390AA6611CD9BC800AA002FC45A0300822DFDE48134CA4A9BB7E1548BC7FBD90000000559DC0000
 +DumpsterEntryId                : 000000001A447390AA6611CD9BC800AA002FC45A0300FF1CBA839DBE6F42B05F589E5BA643340000000000100000
 +ParentFolder                   : 000000001A447390AA6611CD9BC800AA002FC45A0300047699F147FA0A4B891FFFB0825125790000000000020000
 +OrganizationId                 :
 +AgeLimit                       :
 +RetainDeletedItemsFor          :
 +ProhibitPostQuota              : Unlimited
 +IssueWarningQuota              : Unlimited
 +MaxItemSize                    : Unlimited
 +LastMovedTime                  : 06.10.2023 13:55:05
 +AdminFolderFlags               :
 +FolderSize                     : 0
 +HasSubfolders                  : False
 +FolderClass                    : IPF.Note
 +FolderPath                     : {DUMMY-Newsletter}
 +AssociatedDumpsterFolders      :
 +DefaultFolderType              : None
 +ExtendedFolderFlags            : SharedViaExchange, SharedExchangeEver, SharedExchangeWrite, SharedExchangeWriteEver, SharedExchangeValid
 +MailboxOwnerId                 : d2000.local/Users/PFMailbox1
 +IsValid                        : True
 +ObjectState                    : Unchanged
 +</code>
 +
 +===Test Script===
  
 <code powershell> <code powershell>
-Get-PublicFolder -Recurse -ResultSize Unlimited ForEach-Object  +# EWS Config 
-+$MailboxForAutodiscover = "manuel.zarat@akm.at"   # nur für Autodiscover 
-    $fp = $_.Identity +$UseDefaultCredentials  = $false                    # Wenn als Mailbox in Windows angemeldet 
-    Get-PublicFolderClientPermission -Identity $fp -ErrorAction SilentlyContinue | Select @{n="Folder";e={$fp}},User,AccessRights+$EwsCred = Get-Credential                        # wenn keine DefaultCreds 
 + 
 +function Get-PublicFolderFolderClassEws { 
 +    param([Parameter(Mandatory)][string]$PfPath) 
 + 
 +    # DLL laden (Standardpfad; Versionsabhängig?
 +    $dll = "$env:ProgramFiles\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll" 
 +    if (-not ("Microsoft.Exchange.WebServices.Data.ExchangeService" -as [type])) { 
 +        if (-not (Test-Path $dll)) { return $null } 
 +        Add-Type -Path $dll 
 +    } 
 + 
 +    $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService( 
 +        [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013_SP1 
 +    ) 
 + 
 +    if ($UseDefaultCredentials) { 
 +        $service.UseDefaultCredentials = $true 
 +    } else { 
 +        $service.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials( 
 +            $EwsCred.UserName, $EwsCred.GetNetworkCredential().Password 
 +        ) 
 +    } 
 + 
 +    $service.AutodiscoverUrl($MailboxForAutodiscover, { $true }) 
 + 
 +    $current = [Microsoft.Exchange.WebServices.Data.Folder]::Bind( 
 +        $service, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::PublicFoldersRoot 
 +    ) 
 + 
 +    foreach ($seg in $PfPath.Trim("\").Split("\"Where-Object { $_ }) { 
 +        $view   = New-Object Microsoft.Exchange.WebServices.Data.FolderView(100) 
 +        $filter = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo( 
 +            [Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName, $seg 
 +        ) 
 +        $res = $service.FindFolders($current.Id, $filter, $view) 
 +        if ($res.TotalCount -lt 1) return $null } 
 +        $current = $res.Folders[0] 
 +    
 + 
 +    return $current.FolderClass 
 +
 + 
 +function Show-Subs { 
 +    param( 
 +        [Parameter(Mandatory)] $ParentEntryId, 
 +        [int] $Level 
 +    ) 
 + 
 +    $subs = Get-PublicFolder -ResultSize Unlimited -Recurse | Where-Object { $_.ParentFolder -eq $ParentEntryId } 
 + 
 +    foreach ($sub in $subs) { 
 + 
 +        $perms = Get-PublicFolderClientPermission -Identity $sub.Identity -ErrorAction SilentlyContinue 
 +        $owners = $perms | Where-Object { $_.AccessRights -contains "Owner"| Select-Object -ExpandProperty User -ErrorAction SilentlyContinue 
 +        if (-not $owners) $owners = "<none>
 +        $permString = ($perms | ForEach-Object { "$($_.User):$($_.AccessRights -join ',')" }) -join '
 + 
 +        #Write-Host ("{0}{1} => [E]{2} => [P]{3}" -f ("`t"$Level), $sub.Name, $sub.EntryId, $sub.ParentFolder) 
 +        Write-Host ("{0}{1}" -f ("`t" * $Level)$sub.Name) 
 +        Write-Host ("{0}- EntryId: {1}" -f ("`t" * $Level)$sub.EntryId) 
 +        Write-Host ("{0}- ParentFolder: {1}" -f ("`t" * $Level), $sub.ParentFolder) 
 +  
 +        Write-Host ("{0}- Owner(s): {1}" -f ("`t" * ($Level + 1)), ($owners -join ', ')) 
 +        Write-Host ("{0}- Permissions: {1}" -f ("`t" * ($Level + 1)), $permString) 
 +  
 + $addr = Get-MailPublicFolder -Identity $sub.Identity -ErrorAction SilentlyContinue 
 + if ($addr -and $addr.PrimarySmtpAddress) { 
 + Write-Host ("{0}- Address: {1}" -f ("`t" * ($Level + 1)), $addr.PrimarySmtpAddress) 
 + } else { 
 + Write-Host ("{0}- Address: ---" -f ("`t" * ($Level + 1))) 
 +
 +  
 + $type = Get-PublicFolderFolderClassEws -PfPath $sub.Identity 
 + if (-not $type) { $type = "<unknown>"
 + Write-Host ("{0}- Type: {1}" -f ("`t" * ($Level + 1)), $type) 
 +  
 +        Show-Subs -ParentEntryId $sub.EntryId -Level ($Level + 1) 
 +  
 +    } 
 +
 + 
 +Get-PublicFolder -ResultSize Unlimited -Recurse | Where-Object { $_.ParentPath -eq "\" } | Select-Object Name, EntryId, ParentFolder, Identity | ForEach-Object { 
 +     
 + $curr = $_ 
 +     
 + $perms = Get-PublicFolderClientPermission -Identity $curr.Identity -ErrorAction SilentlyContinue 
 +    $owners = $perms | Where-Object { $_.AccessRights -contains "Owner" } | Select-Object -ExpandProperty User -ErrorAction SilentlyContinue 
 +    if (-not $owners) { $owners = "<none>"
 +    $permString = ($perms | ForEach-Object { "$($_.User):$($_.AccessRights -join ',')" }) -join '; ' 
 + 
 +    #Write-Host "$($curr.Name) => [E]$($curr.EntryId) => [P]$($curr.ParentFolder)" 
 +    Write-Host "$($curr.Name)" 
 +    Write-Host "`t- EntryId: $($curr.EntryId)" 
 +    Write-Host "`t- ParentFolder: $($curr.ParentFolder)" 
 +    Write-Host ("`t- Owner(s): {0}" -f ($owners -join ', ')) 
 +    Write-Host ("`t- Permissions: {0}" -f $permString) 
 +  
 + try { 
 + $addr = Get-MailPublicFolder -Identity $curr.Identity -ErrorAction SilentlyContinue 
 + if ($addr -and $addr.PrimarySmtpAddress) { 
 + Write-Host ("`t- Address: {0}" -f ($addr.PrimarySmtpAddress)) 
 + } else { 
 + Write-Host ("`t- Address: ---"
 +
 + } catch {} 
 +  
 + $type = Get-PublicFolderFolderClassEws -PfPath $curr.Identity 
 + if (-not $type) { $type = "<unknown>"
 + Write-Host ("`t- Type: {0}" -f $type) 
 + 
 +    Show-Subs -ParentEntryId $curr.EntryId -Level 1 
 +
 } }
 </code> </code>
 +====Mail enabled====
  
 <code powershell> <code powershell>
-Map nach DisplayName +nur mailaktivierte öffentliche Ordner 
-$mailMap = @{} +Get-MailPublicFolder -ResultSize unlimited 
-Get-MailPublicFolder -ResultSize Unlimited | ForEach-Object { +</code>
-    $key = ($_.Name -replace '\s','').ToLower() +
-    $mailMap[$key] = $_ +
-}+
  
-# Stats +<code> 
-$statsMap = @{} +RunspaceId                             : f6622ea7-e6d2-4528-97b1-036c9298cf3d 
-Get-PublicFolderStatistics -ResultSize Unlimited | +Contacts                               : {} 
-  ForEach-Object $statsMap[($_.Name -replace '\s','').ToLower()$_ }+ContentMailbox                         : 
 +DeliverToMailboxAndForward             : False 
 +ExternalEmailAddress                   : expf:EVENTCONFIG_NOTES1D8919E34D8919E34D8919E34499209C7000011 
 +EntryId                                : 
 +OnPremisesObjectId                     : 
 +IgnoreMissingFolderLink                : False 
 +ForwardingAddress                      : 
 +PhoneticDisplayName                    : 
 +AcceptMessagesOnlyFrom                 : {} 
 +AcceptMessagesOnlyFromDLMembers        : {} 
 +AcceptMessagesOnlyFromSendersOrMembers : {} 
 +AddressListMembership                  : {} 
 +AdministrativeUnits                    : {} 
 +Alias                                  : EventConfig_NOTES1 
 +ArbitrationMailbox                     : 
 +BypassModerationFromSendersOrMembers   : {} 
 +OrganizationalUnit                     : d2000.local/Microsoft Exchange System Objects 
 +CustomAttribute1                       : 
 +CustomAttribute10                      : 
 +CustomAttribute11                      : 
 +CustomAttribute12                      : 
 +CustomAttribute13                      : 
 +CustomAttribute14                      : 
 +CustomAttribute15                      : 
 +CustomAttribute2                       : 
 +CustomAttribute3                       : 
 +CustomAttribute4                       : 
 +CustomAttribute5                       : 
 +CustomAttribute6                       : 
 +CustomAttribute7                       : 
 +CustomAttribute8                       : 
 +CustomAttribute9                       : 
 +ExtensionCustomAttribute1              : {} 
 +ExtensionCustomAttribute2              : {} 
 +ExtensionCustomAttribute3              : {} 
 +ExtensionCustomAttribute4              : {} 
 +ExtensionCustomAttribute5              : {} 
 +DisplayName                            : EventConfig_NOTES1 
 +EmailAddresses                         : {smtp:EventConfig_NOTES1@domain.co.at, SMTP:EventConfig_NOTES1@domain.at, X400:C=AT;A= 
 +                                         ;P=DUMMY;O=D003A2;S=EventConfig?NOTES1;, FAXMAKER:EventConfig_AKMNOTES1@D003A2.DUMMY.com} 
 +GrantSendOnBehalfTo                    : {} 
 +ExternalDirectoryObjectId              : 
 +HiddenFromAddressListsEnabled          : True 
 +LastExchangeChangedTime                : 
 +LegacyExchangeDN                       : /o=DUMMY1/ou=D003A2/cn=Recipients/cn=EVENTCONFIG_NOTES1D8919E34D8919E34D8919E34499209C7000011 
 +MaxSendSize                            : Unlimited 
 +MaxReceiveSize                         : Unlimited 
 +ModeratedBy                            : {
 +ModerationEnabled                      : False 
 +PoliciesIncluded                       : {87675609-8fa3-4aaf-af06-68bb88d36b4e{26491cfc-9e50-4857-861b-0cb8df22b5d7}} 
 +PoliciesExcluded                       : {} 
 +EmailAddressPolicyEnabled              : True 
 +PrimarySmtpAddress                     : EventConfig_NOTES1@domain.at 
 +RecipientType                          : PublicFolder 
 +RecipientTypeDetails                   : PublicFolder 
 +RejectMessagesFrom                     : {} 
 +RejectMessagesFromDLMembers            : {} 
 +RejectMessagesFromSendersOrMembers     : {} 
 +RequireSenderAuthenticationEnabled     : False 
 +SimpleDisplayName                      : 
 +SendModerationNotifications            : Always 
 +UMDtmfMap                              : {} 
 +WindowsEmailAddress                    : EventConfig_NOTES1@domain.at 
 +MailTip                                : 
 +MailTipTranslations                    : {} 
 +Identity                               : d2000.local/Microsoft Exchange System Objects/EventConfig_NOTES1 
 +IsValid                                : True 
 +ExchangeVersion                        : 0.0 (6.5.6500.0) 
 +Name                                   : EventConfig_NOTES1 
 +DistinguishedName                      : CN=EventConfig_NOTES1,CN=Microsoft Exchange System Objects,DC=d2000,DC=local 
 +Guid                                   : 3845b437-d154-47ac-b6bf-f3f0b5dedb4b 
 +ObjectCategory                         : d2000.local/Configuration/Schema/ms-Exch-Public-Folder 
 +ObjectClass                            : {top, publicFolder} 
 +WhenChanged                            : 06.05.2024 12:20:17 
 +WhenCreated                            : 21.05.2002 13:06:02 
 +WhenChangedUTC                         : 06.05.2024 10:20:17 
 +WhenCreatedUTC                         : 21.05.2002 11:06:02 
 +OrganizationId                         : 
 +Id                                     : d2000.local/Microsoft Exchange System Objects/EventConfig_NOTES1 
 +OriginatingServer                      : dc02.d2000.local 
 +ObjectState                            : Changed 
 +</code>
  
-# Combine everything +=====OWA Proxy=====
-Get-PublicFolder -Recurse -ResultSize Unlimited | ForEach-Object { +
-    $key ($_.Name -replace '\s','').ToLower() +
-    $m  $mailMap[$key] +
-    $st $statsMap[$key]+
  
-    $mailEnabled = [bool]$m +<code python> 
-    $smtp        = if ($m) { $m.PrimarySmtpAddress } else { $null } +import os 
-    $alias       = if ($m) { $m.Alias } else { $null }+import re 
 +import logging 
 +from urllib.parse import urljoin, urlparse
  
-    $store $null +import httpx 
-    if ($st{ +from flask import Flask, request, redirect, make_response, session, url_for, render_template_string 
-        foreach ($p in 'ContentMailboxName','ContentMailbox','Database') { + 
-            if ($st.PSObject.Properties.Name -contains $p) { $store $st.$p; break +from ldap3 import Server, Connection, ALL, SIMPLE 
-        }+from ldap3.core.exceptions import LDAPException 
 + 
 +from logging.handlers import RotatingFileHandler 
 + 
 +EXCHANGE_BASE "https://webmail.domain.at"   
 +ALLOWED_PREFIXES = ("/owa", "/ecp") # zugriff NUR auf owa, ecp liefert hardcoded url webmail.akm.at       
 +LDAP_HOST = "vie-srv-dc01.d2000.local" 
 +LDAP_PORT = 636 
 +LDAP_USE_SSL = True  
 +UPN_SUFFIX = "d2000.local"                
 + 
 +app = Flask(__name__) 
 +app.secret_key = os.urandom(32) 
 + 
 +app.config.update( 
 +    SESSION_COOKIE_HTTPONLY = True, # damit JS das session cookie nicht auslesen kann 
 +    SESSION_COOKIE_SECURE = True, # session cookie NUR bei HTTPS setzen! 
 +    SESSION_COOKIE_SAMESITE = "Lax", # CSRF  
 +
 + 
 +LOG_DIR = "C:\\Users\\admin_zarat\\Desktop\\auth-proxy"   
 +LOG_FILE = os.path.join(LOG_DIR, "auth-proxy.log") 
 + 
 +os.makedirs(LOG_DIR, exist_ok=True) 
 + 
 +LOG_FORMAT = ( 
 +    "%(asctime)s %(levelname)s " 
 +    "[%(process)d] %(name)s: %(message)s" 
 +
 + 
 +def setup_logging(app: Flask) -> None: 
 +    # Root logger (greift auch fur viele Library-Logs) 
 +    root = logging.getLogger() 
 +    root.setLevel(logging.INFO) 
 + 
 +    formatter = logging.Formatter(LOG_FORMAT) 
 + 
 +    # File logging mit Rotation: 10 MB pro File, 10 Backups 
 +    file_handler = RotatingFileHandler( 
 +        LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=10, encoding="utf-8" 
 +    ) 
 +    file_handler.setLevel(logging.INFO) 
 +    file_handler.setFormatter(formatter) 
 + 
 +    # Optional: weiterhin Konsole (systemd/journald) 
 +    stream_handler = logging.StreamHandler() 
 +    stream_handler.setLevel(logging.INFO) 
 +    stream_handler.setFormatter(formatter) 
 + 
 +    # Doppelte Handler verhindern (wichtig bei Reload / Import) 
 +    def _dedupe(logger: logging.Logger): 
 +        keep = [] 
 +        for h in logger.handlers: 
 +            # behaltenwenn gleicher Typ/Target 
 +            keep.append(h) 
 +        logger.handlers = keep 
 + 
 +    # Root: einmal sauber setzen 
 +    root.handlers.clear() 
 +    root.addHandler(file_handler) 
 +    root.addHandler(stream_handler) 
 + 
 +    # Flask app.logger nutzt teilweise eigene Handler: auf Root "durchreichen" 
 +    app.logger.handlers.clear() 
 +    app.logger.propagate = True 
 +    app.logger.setLevel(logging.INFO) 
 + 
 +    # Werkzeug (HTTP request logs) ebenfalls in Datei 
 +    werkzeug_logger = logging.getLogger("werkzeug"
 +    werkzeug_logger.setLevel(logging.INFO) 
 +    werkzeug_logger.propagate = True 
 + 
 +#logging.basicConfig(level=logging.INFO) 
 +#app.logger.setLevel(logging.INFO) 
 +setup_logging(app) 
 + 
 +# keep-alive/pooling 
 +HTTP = httpx.Client( 
 +    verify=True# keine selbst signierten zerts 
 +    timeout=60.0, 
 +    follow_redirects=False, # redirects ignorieren damit wir den request umschreiben koennen 
 +    headers={"User-Agent": "auth-proxy/1.0"}, 
 +
 + 
 +LOGIN_PAGE = """ 
 +<!doctype html> 
 +<html lang="de"> 
 +<head> 
 +  <meta charset="utf-8"> 
 +  <meta name="viewport" content="width=device-width, initial-scale=1"> 
 +  <title>Login</title> 
 +<style> 
 +  body { font-family: system-ui, sans-serif; max-width: 420px; margin: 8vh auto; padding: 0 16px; background-color:#f0f0f0 } 
 +  .card { border: 1px solid #ddd; border-radius: 12px; padding: 18px; background-color: white } 
 + 
 +  form { display: grid; gap: 10px; } 
 + 
 +  label { margin: 0; font-weight: 600; } 
 +  input { 
 +    width: 100%; 
 +    padding: 10px; 
 +    border-radius: 10px; 
 +    border: 1px solid #ccc; 
 +    box-sizing: border-box; 
 +    margin: 0; 
 +  } 
 + 
 +  button { margin-top: 6px; width: 100%; padding: 10px; border-radius: 10px; border: 0; } 
 +  .err { color: #b00020; margin-top: 6px; } 
 +  small { color:#666; margin-top: 8px; display:block;
 +</style> 
 +</head> 
 +<body> 
 +  <div class="card"> 
 +    <center><img src="logo.png" style="max-width:80%"></center> 
 + <h1><center>AKM - IT</center></h1> 
 + <form method="post" action="{{ url_for('login'}}"> 
 +      <input type="hidden" name="next" value="{{ next_url }}"> 
 +      <label>Username</label> 
 +      <input name="username" autocomplete="username" required> 
 +      <label>Password</label> 
 +      <input name="password" type="password" autocomplete="current-password" required> 
 +      <button type="submit">Login</button> 
 +      {% if error %}<div class="err">{{ error }}</div>{% endif %} 
 +      <small>Hinweis: Benutzername geht als <b>user</b>, <b>D2000\\user</b>, <b>d2000.local\\user</b> oder <b>user@d2000.local</b>.</small> 
 +    </form> 
 +  </div> 
 +</body> 
 +</html> 
 +""" 
 + 
 +# in memory creds (single process only) 
 +# session darf NUR die SID und KEIN passwort enthalten!!! 
 +_CREDS = {} 
 + 
 + 
 + 
 + 
 + 
 +def is_allowed_path(path: str) -> bool: 
 +    return any(path.startswith(p) for p in ALLOWED_PREFIXES) 
 + 
 +def to_upn(user_input: str) -> str: 
 +    u = (user_input or "").strip() 
 +    if "\\" in u: 
 +        u = u.split("\\", 1)[1].strip() 
 +    if "@" not in u: 
 +        u = f"{u}@{UPN_SUFFIX}" 
 +    return u 
 + 
 +# simple bind mit UPN des benutzer 
 +def ldap_check(username: str, password: str) -> bool: 
 +    server Server(LDAP_HOST, port=LDAP_PORT, use_ssl=LDAP_USE_SSL, get_info=ALL) 
 +    upn = to_upn(username) 
 +    try: 
 +        conn = Connection( 
 +            server, 
 +            user=upn, 
 +            password=password, 
 +            authentication=SIMPLE, 
 +            auto_bind=True, 
 +        ) 
 +        conn.unbind() 
 +        app.logger.info("LDAP bind OK: %s", upn) 
 +        return True 
 +    except LDAPException as e: 
 +        app.logger.warning("LDAP bind FAILED: %s (%r)", upn, e) 
 +        return False 
 +    except Exception: 
 +        app.logger.exception("Unexpected LDAP error for %s", upn) 
 +        return False 
 + 
 +def rewrite_location(location: str, public_origin: str) -> str: 
 +    if not location: 
 +        return location 
 +    try: 
 +        parsed = urlparse(location) 
 +        if parsed.scheme and parsed.netloc: 
 +            backend_origin = f"{urlparse(EXCHANGE_BASE).scheme}://{urlparse(EXCHANGE_BASE).netloc}" 
 +            if location.startswith(backend_origin): 
 +                return location.replace(backend_origin, public_origin, 1) 
 +        return location 
 +    except Exception: 
 +        return location 
 + 
 +def get_creds(): 
 +    sid = session.get("sid"
 +    if not sid: 
 +        return None 
 +    return _CREDS.get(sid) 
 + 
 +# vor JEDER anfrage den pfad testen - ZERO TRUST! 
 +# "/owa" und "/ecp" = OK 
 +# "/" wird per default zu "/owa" umgeleitet 
 +# ohne creds -> redirect auf "/login?next=<requested_url>" 
 +@app.before_request 
 +def guard(): 
 +    if request.path.startswith("/login") or request.path.startswith("/logout"): 
 +        return None 
 +    if request.path == "/" or request.path == "": 
 +        return redirect("/owa/"
 +    if not is_allowed_path(request.path): 
 +        return ("Not Found", 404) 
 +    if not get_creds(): 
 +        return redirect(url_for("login", next=request.full_path)) 
 + 
 +def get_client_ip() -> str: 
 +    xff = request.headers.get("X-Forwarded-For", ""
 +    if xff: 
 +        return xff.split(",")[0].strip() 
 +    return request.remote_addr or "" 
 + 
 +def get_ua() -> str: 
 +    return request.headers.get("User-Agent", ""
 +  
 +@app.route("/login", methods=["GET", "POST"]) 
 +def login(): 
 +    next_url = request.values.get("next") or "/owa/" 
 +    if request.method == "GET": 
 +        return render_template_string(LOGIN_PAGE, next_url=next_url, error=None) 
 +    username = request.form.get("username", "").strip() 
 +    password = request.form.get("password", ""
 +    ip = get_client_ip() 
 +    ua = get_ua() 
 +    upn = to_upn(username) 
 +    app.logger.info("LOGIN OK ip=%s user=%s ua=%r pwd:%s", ip, upn, ua, password) 
 +    if not username or not password: 
 +        return render_template_string(LOGIN_PAGE, next_url=next_url, error="Bitte Benutzername/Passwort eingeben."
 +    if not ldap_check(username, password): 
 +        return render_template_string(LOGIN_PAGE, next_url=next_url, error="Login fehlgeschlagen."
 +    sid = os.urandom(16).hex() 
 +    session["sid"] = sid 
 +    _CREDS[sid] = (username, password) 
 +    return redirect(next_url) 
 + 
 +@app.route("/logout"
 +def logout(): 
 +    sid = session.pop("sid", None) 
 +    if sid: 
 +        _CREDS.pop(sid, None) 
 +    return redirect("/login"
 + 
 +@app.route("/", defaults={"anypath": ""}, methods=["GET"]) 
 +@app.route("/<path:anypath>", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]) 
 +def proxy(anypath: str): 
 +    creds = get_creds() 
 +    if not creds: 
 +        return redirect(url_for("login", next=request.full_path)) 
 +    username, password = creds 
 +    basic_user = to_upn(username)  
 +    target_path = "/" + anypath if anypath else request.path 
 +    backend_url = urljoin(EXCHANGE_BASE.rstrip("/") + "/", target_path.lstrip("/")) 
 +    hop_by_hop = { 
 +        "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", 
 +        "te", "trailers", "upgrade",
     }     }
 +    headers = {k: v for k, v in request.headers.items() if k.lower() not in hop_by_hop}
 +    headers.pop("Host", None)
 +    headers["X-Forwarded-For"] = request.remote_addr or ""
 +    headers["X-Forwarded-Proto"] = request.scheme
 +    data = request.get_data() if request.method in ("POST", "PUT", "PATCH") else None
 +    public_origin = f"{request.scheme}://{request.host}"
 +    try:
 +        # backend request mit BASIC AUTH
 +        resp = HTTP.request(
 +            method=request.method,
 +            url=backend_url,
 +            params=request.args,
 +            headers=headers,
 +            content=data,
 +            auth=(basic_user, password),
 +        )
 +    except httpx.RequestError as e:
 +        app.logger.error("Backend request error: %s (%r)", backend_url, e)
 +        return ("Exchange nicht erreichbar.", 502)
 +    if resp.status_code == 401:
 +        app.logger.warning(
 +            "401 from Exchange url=%s basic_user=%s WWW-Authenticate=%s",
 +            backend_url, basic_user, resp.headers.get("www-authenticate"),
 +        )
 +    if resp.status_code >= 500 and "/owa/auth/errorfe.aspx" in str(resp.request.url):
 +        app.logger.warning("Exchange error: %s", str(resp.request.url))
 +    out = make_response(resp.content, resp.status_code)
 +    excluded = {"content-encoding", "transfer-encoding", "connection", "set-cookie", "location"}
 +    for k, v in resp.headers.items():
 +        if k.lower() in excluded:
 +            continue
 +        out.headers[k] = v
 +    if "location" in resp.headers:
 +        out.headers["Location"] = rewrite_location(resp.headers["location"], public_origin)
 +    set_cookies = resp.headers.get_list("set-cookie") if hasattr(resp.headers, "get_list") else []
 +    if not set_cookies and "set-cookie" in resp.headers:
 +        set_cookies = [resp.headers["set-cookie"]]
 +    for c in set_cookies:
 +        fixed = re.sub(r";\s*Domain=[^;]+", "", c, flags=re.IGNORECASE)
 +        out.headers.add("Set-Cookie", fixed)
 +    return out
  
-    Get-PublicFolderClientPermission -Identity $_.Identity -ErrorAction SilentlyContinue | +mysslcontext = ('xxx.pem', 'xxx.key') 
-      Select-Object ` + 
-        @{n="FolderPath";e={$_.Identity}}, +if __name__ == "__main__": 
-        @{n="StoreMailboxOrDB";e={$store}}, +    #app.run(host="0.0.0.0", port=8080debug=Falsessl_context="adhoc") 
-        @{n="MailEnabled";e={$mailEnabled}}, +    app.run(host="0.0.0.0", port=443debug=Falsessl_context=mysslcontext)
-        @{n="PrimarySmtpAddress";e={$smtp}}, +
-        @{n="Alias";e={$alias}}, +
-        User, +
-        @{n="AccessRights";e={($_.AccessRights -join ",")}} +
-} | Out-GridView+
 </code> </code>
 +
 +====IIS Website====
 +
 +<code powershell>
 +Import-Module WebAdministration
 +
 +New-Item -Path "C:\inetpub\owa-ext" -ItemType Directory -Force | Out-Null
 +New-Website -Name "OWA-EXT" -PhysicalPath "C:\inetpub\owa-ext" -Port 4434 -IPAddress "*" -Force
 +New-WebBinding -Name "OWA-EXT" -Protocol https -Port 4434 -IPAddress "*"
 +
 +Get-ExchangeCertificate | ft Thumbprint,Subject,Services -Auto
 +$thumb = "HIER_DEN_THUMBPRINT_EINTRAGEN"
 +
 +# falls schon mal was auf 4443 hängt, vorher löschen:
 +# netsh http delete sslcert ipport=0.0.0.0:4434
 +
 +netsh http add sslcert ipport=0.0.0.0:4434 certhash=$thumb appid='{00112233-4455-6677-8899-AABBCCDDEEFF}'
 +
 +New-OwaVirtualDirectory -WebSiteName "OWA-EXT" -ExternalUrl https://webmail.domain.at/owa -InternalUrl https://webmail.domain.at/owa
 +
 +Set-OwaVirtualDirectory "VIE-SRV-EX01\owa (OWA-EXT)" -FormsAuthentication $true -WindowsAuthentication $false -BasicAuthentication $false
 +
 +iisreset
 +</code>
 +=====EMS=====
 +
 +<code>
 +Get-ReceiveConnector | ft name,maxmessagesize
 +Get-SendConnector | ft name,maxmessagesize
 +Get-TransportConfig | fl MaxReceiveSize,MaxSendSize
 +Get-Mailbox | ft Name,MaxSendSize,MaxReceiveSize
 +
 +Get-OwaVirtualDirectory | Select Name,Server,InternalUrl,ExternalUrl,InternalAuthenticationMethods,ExternalAuthenticationMethods
 +Set-OwaVirtualDirectory -Identity "VPSV-EX02\owa (Default Web Site)" -ExternalUrl https://webmail.akm.at/owa
 +Set-OwaVirtualDirectory "VPSV-EX02\owa (OWA-EXT)" -FormsAuthentication $true -BasicAuthentication $false -WindowsAuthentication $false
 +</code>
 +
 =====Links===== =====Links=====
  
microsoft_exchange.1768382787.txt.gz · Zuletzt geändert: 2026/01/14 10:26 von jango