Created
July 3, 2026 16:19
-
-
Save eugrus/12e670129a6e41bf498d5ac2df84d71d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| param( | |
| [string]$Path = '.', | |
| [switch]$WhatIf | |
| ) | |
| if (-not ('MsgReader' -as [type])) { | |
| Add-Type -TypeDefinition @' | |
| using System; | |
| using System.Text; | |
| using System.Runtime.InteropServices; | |
| using System.Runtime.InteropServices.ComTypes; | |
| public static class MsgReader | |
| { | |
| [DllImport("ole32.dll")] | |
| private static extern int StgOpenStorage( | |
| [MarshalAs(UnmanagedType.LPWStr)] string pwcsName, | |
| IStorage pstgPriority, uint grfMode, IntPtr snbExclude, | |
| uint reserved, out IStorage ppstgOpen); | |
| [ComImport, Guid("0000000B-0000-0000-C000-000000000046"), | |
| InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] | |
| private interface IStorage | |
| { | |
| void CreateStream(string pwcsName, uint grfMode, uint r1, uint r2, out IStream ppstm); | |
| void OpenStream(string pwcsName, IntPtr r1, uint grfMode, uint r2, out IStream ppstm); | |
| void CreateStorage(string pwcsName, uint grfMode, uint r1, uint r2, out IStorage ppstg); | |
| void OpenStorage(string pwcsName, IStorage pstgPriority, uint grfMode, IntPtr snbExclude, uint reserved, out IStorage ppstg); | |
| void CopyTo(uint ciidExclude, IntPtr rgiidExclude, IntPtr snbExclude, IStorage pstgDest); | |
| void MoveElementTo(string pwcsName, IStorage pstgDest, string pwcsNewName, uint grfFlags); | |
| void Commit(uint grfCommitFlags); | |
| void Revert(); | |
| void EnumElements(uint r1, IntPtr r2, uint r3, out IntPtr ppenum); | |
| void DestroyElement(string pwcsName); | |
| void RenameElement(string pwcsOldName, string pwcsNewName); | |
| void SetElementTimes(string pwcsName, IntPtr pctime, IntPtr patime, IntPtr pmtime); | |
| void SetClass(ref Guid clsid); | |
| void SetStateBits(uint grfStateBits, uint grfMask); | |
| void Stat(out System.Runtime.InteropServices.ComTypes.STATSTG pstatstg, uint grfStatFlag); | |
| } | |
| private const uint STGM_READ = 0x00000000; | |
| private const uint STGM_SHARE_DENY_WRITE = 0x00000020; | |
| private const uint STGM_SHARE_EXCLUSIVE = 0x00000010; | |
| private static byte[] ReadStream(IStorage stg, string name) | |
| { | |
| IStream stm; | |
| try { stg.OpenStream(name, IntPtr.Zero, STGM_READ | STGM_SHARE_EXCLUSIVE, 0, out stm); } | |
| catch { return null; } | |
| try | |
| { | |
| System.Runtime.InteropServices.ComTypes.STATSTG st; | |
| stm.Stat(out st, 1); | |
| byte[] buf = new byte[(int)st.cbSize]; | |
| stm.Read(buf, buf.Length, IntPtr.Zero); | |
| return buf; | |
| } | |
| finally { Marshal.ReleaseComObject(stm); } | |
| } | |
| private static string ReadStringProp(IStorage stg, string propId) | |
| { | |
| byte[] b = ReadStream(stg, "__substg1.0_" + propId + "001F"); // PT_UNICODE | |
| if (b != null) return Encoding.Unicode.GetString(b).TrimEnd('\0'); | |
| b = ReadStream(stg, "__substg1.0_" + propId + "001E"); // PT_STRING8 | |
| if (b != null) return Encoding.Default.GetString(b).TrimEnd('\0'); | |
| return null; | |
| } | |
| private static string SenderFromTransportHeaders(IStorage stg) | |
| { | |
| string headers = ReadStringProp(stg, "007D"); | |
| if (String.IsNullOrEmpty(headers)) return null; | |
| // Header entfalten (Fortsetzungszeilen beginnen mit Whitespace) | |
| headers = headers.Replace("\r\n ", " ").Replace("\r\n\t", " "); | |
| foreach (string line in headers.Split(new[] { "\r\n" }, StringSplitOptions.None)) | |
| { | |
| if (!line.StartsWith("From:", StringComparison.OrdinalIgnoreCase)) continue; | |
| string v = line.Substring(5).Trim(); | |
| int lt = v.LastIndexOf('<'); | |
| int gt = v.LastIndexOf('>'); | |
| if (lt >= 0 && gt > lt) return v.Substring(lt + 1, gt - lt - 1).Trim(); | |
| if (v.Contains("@")) return v.Trim('"', ' '); | |
| break; | |
| } | |
| return null; | |
| } | |
| private static bool Plausible(long ft) | |
| { | |
| if (ft <= 0) return false; | |
| DateTime dt = DateTime.FromFileTimeUtc(ft); | |
| return dt.Year >= 1990 && dt.Year <= 2199; | |
| } | |
| public static void GetInfo(string path, out DateTime date, out string sender, out string subject) | |
| { | |
| IStorage stg; | |
| int hr = StgOpenStorage(path, null, STGM_READ | STGM_SHARE_DENY_WRITE, IntPtr.Zero, 0, out stg); | |
| if (hr != 0) Marshal.ThrowExceptionForHR(hr); | |
| try | |
| { | |
| byte[] props = ReadStream(stg, "__properties_version1.0"); | |
| if (props == null) throw new InvalidOperationException("Properties stream missing."); | |
| long submit = 0, delivery = 0, creation = 0; | |
| for (int off = 32; off + 16 <= props.Length; off += 16) | |
| { | |
| uint tag = BitConverter.ToUInt32(props, off); | |
| long val = BitConverter.ToInt64(props, off + 8); | |
| if (tag == 0x00390040) submit = val; | |
| else if (tag == 0x0E060040) delivery = val; | |
| else if (tag == 0x30070040) creation = val; | |
| } | |
| long ft = Plausible(submit) ? submit : (Plausible(delivery) ? delivery : creation); | |
| if (!Plausible(ft)) throw new InvalidOperationException("No usable date property found."); | |
| date = DateTime.FromFileTimeUtc(ft).ToLocalTime(); | |
| // Absender-Kaskade: SMTP-Properties -> Transport-Header -> Anzeigename | |
| sender = ReadStringProp(stg, "5D01"); // PidTagSenderSmtpAddress | |
| if (String.IsNullOrEmpty(sender)) | |
| { | |
| string s = ReadStringProp(stg, "0C1F"); // PR_SENDER_EMAIL_ADDRESS | |
| if (s != null && s.Contains("@")) sender = s; | |
| } | |
| if (String.IsNullOrEmpty(sender)) | |
| sender = ReadStringProp(stg, "5D02"); // PidTagSentRepresentingSmtpAddress | |
| if (String.IsNullOrEmpty(sender)) | |
| { | |
| string s = ReadStringProp(stg, "0065"); // PR_SENT_REPRESENTING_EMAIL_ADDRESS | |
| if (s != null && s.Contains("@")) sender = s; | |
| } | |
| if (String.IsNullOrEmpty(sender)) | |
| sender = SenderFromTransportHeaders(stg); // From:-Header, echter SMTP-Weg | |
| if (String.IsNullOrEmpty(sender)) | |
| sender = ReadStringProp(stg, "0C1A"); // Anzeigename als letzter Ausweg | |
| if (String.IsNullOrEmpty(sender)) sender = "unknown"; | |
| subject = ReadStringProp(stg, "0037"); // PR_SUBJECT | |
| if (String.IsNullOrEmpty(subject)) subject = "no subject"; | |
| } | |
| finally { Marshal.ReleaseComObject(stg); } | |
| } | |
| } | |
| '@ | |
| } | |
| function ConvertTo-SafeFileNamePart { | |
| param([string]$Text, [int]$MaxLength) | |
| $invalid = [System.IO.Path]::GetInvalidFileNameChars() -join '' | |
| $safe = $Text -replace ('[{0}]' -f [regex]::Escape($invalid)), '_' | |
| $safe = ($safe -replace '\s+', ' ').Trim(' ', '.', '_') | |
| if ($safe.Length -gt $MaxLength) { $safe = $safe.Substring(0, $MaxLength).TrimEnd(' ', '.', '_') } | |
| if (-not $safe) { $safe = '_' } | |
| return $safe | |
| } | |
| Get-ChildItem -LiteralPath $Path -Filter *.msg | ForEach-Object { | |
| if ($_.Name -match '^\d{4}-\d{2}-\d{2} - ') { return } | |
| try { | |
| $date = [datetime]::MinValue; $sender = ''; $subject = '' | |
| [MsgReader]::GetInfo($_.FullName, [ref]$date, [ref]$sender, [ref]$subject) | |
| $senderPart = ConvertTo-SafeFileNamePart -Text $sender -MaxLength 60 | |
| $subjectPart = ConvertTo-SafeFileNamePart -Text $subject -MaxLength 100 | |
| $baseName = '{0} - {1} - {2}' -f $date.ToString('yyyy-MM-dd'), $senderPart, $subjectPart | |
| $newName = $baseName + '.msg' | |
| $i = 2 | |
| while (Test-Path -LiteralPath (Join-Path $_.DirectoryName $newName)) { | |
| if ($newName -eq $_.Name) { return } # bereits korrekt benannt | |
| $newName = '{0} ({1}).msg' -f $baseName, $i | |
| $i++ | |
| } | |
| if ($WhatIf) { | |
| Write-Host ('{0} -> {1}' -f $_.Name, $newName) | |
| } else { | |
| Rename-Item -LiteralPath $_.FullName -NewName $newName | |
| } | |
| } | |
| catch { | |
| Write-Warning ('{0}: {1}' -f $_.FullName, $PSItem.Exception.Message) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment