Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PstFileAttachmentExporter : Export all file attachments from command line #24

Merged
merged 5 commits into from
May 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,26 @@
# This .gitignore file was automatically created by Microsoft(R) Visual Studio.
################################################################################

/bin/Debug
/obj/Debug
/.vs/XstReader/v14
/bin/Release
/obj/Release
/.vs/XstReader/v15
/Releases
/documents

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/

# Visual Studio 2015/2017 cache/options directory
.vs/

# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
16 changes: 14 additions & 2 deletions Message.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
#if !NETCOREAPP
using System.Windows.Documents;
using System.Windows.Media;
#endif

namespace XstReader
{
Expand Down Expand Up @@ -177,6 +179,8 @@ public void ExportToFile(string fullFileName, XstFile xstFile)
}
else if (ShowRtf)
{
#if !NETCOREAPP

var doc = GetBodyAsFlowDocument();
EmbedRtfPrintHeader(doc);
TextRange content = new TextRange(doc.ContentStart, doc.ContentEnd);
Expand All @@ -186,6 +190,9 @@ public void ExportToFile(string fullFileName, XstFile xstFile)
}
if (Date != null)
File.SetCreationTime(fullFileName, (DateTime)Date);
#else
throw new PlatformNotSupportedException();
#endif
}
else
{
Expand All @@ -200,6 +207,7 @@ public void ExportToFile(string fullFileName, XstFile xstFile)
}
}

#if !NETCOREAPP
public FlowDocument GetBodyAsFlowDocument()
{
FlowDocument doc = new FlowDocument();
Expand All @@ -215,7 +223,7 @@ public FlowDocument GetBodyAsFlowDocument()
//var infoString = System.Windows.Markup.XamlWriter.Save(doc);
return doc;
}

#endif
public string EmbedTextPrintHeader(string body, bool forDisplay = false, bool showEmailType = false)
{
string row = forDisplay ? "{0,-15}\t{1}\r\n" : "{0,-15}{1}\r\n";
Expand Down Expand Up @@ -286,6 +294,8 @@ private bool LookForInsertionPoint(string body, string tag, out int insertAt)
}
}

#if !NETCOREAPP

public void EmbedRtfPrintHeader(FlowDocument doc, bool showEmailType = false)
{
if (doc == null)
Expand Down Expand Up @@ -327,6 +337,8 @@ public void EmbedRtfPrintHeader(FlowDocument doc, bool showEmailType = false)
//omit MyName and the line under it for now, as we have no reliable source for it
//doc.Blocks.InsertBefore(doc.Blocks.FirstBlock, p);
}
#endif
#if !NETCOREAPP

private void AddRtfTableRow(Table table, string c0, string c1)
{
Expand All @@ -338,7 +350,7 @@ private void AddRtfTableRow(Table table, string c0, string c1)
currentRow.Cells.Add(new TableCell(new Paragraph(new Run(c1))
{ FontFamily = new FontFamily("Arial"), FontSize = 12 }));
}

#endif
public string EmbedAttachments(string body, XstFile xst)
{
if (body == null)
Expand Down
17 changes: 17 additions & 0 deletions PstFileAttachmentExporter.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PstFileAttachmentExporter", "PstFileAttachmentExporter\PstFileAttachmentExporter.csproj", "{9F60A682-39DE-4E68-B44D-05254DBDEB7F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9F60A682-39DE-4E68-B44D-05254DBDEB7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9F60A682-39DE-4E68-B44D-05254DBDEB7F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9F60A682-39DE-4E68-B44D-05254DBDEB7F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9F60A682-39DE-4E68-B44D-05254DBDEB7F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
100 changes: 100 additions & 0 deletions PstFileAttachmentExporter/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System;
using XstReader;

namespace PstFileExporter
{
class Program
{
static void Main(string[] args)
{
if (args.Length != 1)
{
Console.WriteLine("Export all attachments from a given pst file");
Console.WriteLine("- Give the path of a pst file as first argument");
Console.WriteLine("- All attachments will be extracted in the same directory, respecting the directory structure inside the pst");
return;
}

var fileName = args[0];
var exportDirectory = CreateDirectoryIfNeeded(System.IO.Path.GetDirectoryName(fileName),
System.IO.Path.GetFileNameWithoutExtension(fileName) + "_Export");

var xstView = new XstReader.View();
var xstFile = new XstReader.XstFile(xstView, fileName);
xstFile.ReadFolderTree();
foreach (var folder in xstView.RootFolders)
{
ExtractAttachmentsInFolder(xstView, xstFile, folder, exportDirectory);
}

Console.WriteLine("Done!");

}

private static string CreateDirectoryIfNeeded(string rootDirName, string dirName)
{
string exportDirectory = System.IO.Path.Combine(rootDirName,
RemoveInvalidChars(System.IO.Path.GetFileName(dirName)));
if (!System.IO.Directory.Exists(exportDirectory))
System.IO.Directory.CreateDirectory(exportDirectory);
return exportDirectory;
}



private static string RemoveInvalidChars(string filename)
{
return string.Concat(filename.Split(System.IO.Path.GetInvalidFileNameChars())).TrimEnd().TrimEnd('.');
}

private static void ExtractAttachmentsInFolder(View xstView, XstFile xstFile, XstReader.Folder folder, string exportDirectoryBase)
{
var exportDirectory = CreateDirectoryIfNeeded(exportDirectoryBase, RemoveInvalidChars(folder.Name));

xstFile.ReadMessages(folder);
foreach (var message in folder.Messages)
{
xstFile.ReadMessageDetails(message);
foreach(var att in message.Attachments)
{
if (att.IsFile)
{
var attachmentExpectedName = System.IO.Path.Combine(exportDirectory, att.FileName);
var fi = new System.IO.FileInfo(attachmentExpectedName);
var actionName = string.Empty;

if (!fi.Exists)
{
actionName = "Create";
} else
{
if (fi.CreationTime < message.Received)
{
actionName = "CreateNewer";
}
else {
actionName = "Skip";
}
}
Console.WriteLine(String.Format("{0} : {1}" , actionName, attachmentExpectedName));
switch (actionName)
{
case "Create":
case "CreateNewer":
xstFile.SaveAttachment(attachmentExpectedName, message.Received, att);
break;
default:
break;
}
}
}
}

foreach(var subFolder in folder.Folders)
{
ExtractAttachmentsInFolder(xstView, xstFile, subFolder, exportDirectory);
}

}
}
}
68 changes: 68 additions & 0 deletions PstFileAttachmentExporter/PstFileAttachmentExporter.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\BTree.cs">
<Link>XstReader\BTree.cs</Link>
</Compile>
<Compile Include="..\XstFile.cs">
<Link>XstReader\XstFile.cs</Link>
</Compile>
<Compile Include="..\View.cs">
<Link>XstReader\View.cs</Link>
</Compile>
<Compile Include="..\StandardProperties.cs">
<Link>XstReader\StandardProperties.cs</Link>
</Compile>
<Compile Include="..\RtfDecompressor.cs">
<Link>XstReader\RtfDecompressor.cs</Link>
</Compile>
<Compile Include="..\Property.cs">
<Link>XstReader\Property.cs</Link>
</Compile>
<Compile Include="..\NamedProperties.cs">
<Link>XstReader\NamedProperties.cs</Link>
</Compile>
<Compile Include="..\NDB.cs">
<Link>XstReader\NDB.cs</Link>
</Compile>
<Compile Include="..\Message.cs">
<Link>XstReader\Message.cs</Link>
</Compile>
<Compile Include="..\Map.cs">
<Link>XstReader\Map.cs</Link>
</Compile>
<Compile Include="..\LayoutsU4K.cs">
<Link>XstReader\LayoutsU4K.cs</Link>
</Compile>
<Compile Include="..\LayoutsU.cs">
<Link>XstReader\LayoutsU.cs</Link>
</Compile>
<Compile Include="..\LayoutsA.cs">
<Link>XstReader\LayoutsA.cs</Link>
</Compile>
<Compile Include="..\Layouts.cs">
<Link>XstReader\Layouts.cs</Link>
</Compile>
<Compile Include="..\LTP.cs">
<Link>XstReader\LTP.cs</Link>
</Compile>
<Compile Include="..\Extensions.cs">
<Link>XstReader\Extensions.cs</Link>
</Compile>
<Compile Include="..\Crypto.cs">
<Link>XstReader\Crypto.cs</Link>
</Compile>
</ItemGroup>
</Project>
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ It requires only .Net Framework 4, which is installed by default on Windows 8.1

Xst Reader is based on Microsoft’s documentation of the Outlook file formats in [MS-PST], first published in 2010 as part of the anti-trust settlement with the DOJ and the EU: <https://msdn.microsoft.com/en-us/library/ff385210(v=office.12).aspx>

## PstFileAttachmentExporter

Export all attachments from a given pst file from command line.
This tool is written in .Net Core for maximum portability and is using Xst Reader classes without the UI. It has been tested on MacOs Catalina with VisualStudio for Mac.

1. Open a Command Line
2. cd PstFileAttachmentExporter
3. dotnet build
4. dotnet run path_to_a_pst_file.pst
5. All attachments will be extracted in the pst directory, respecting the directory structure inside the pst. We keep only the last version (overwrite old version of same attachment from different emails)

## Installation

To install a binary:
Expand Down
2 changes: 2 additions & 0 deletions View.cs
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,9 @@ public string Description
}

public bool Hide { get { return (Hidden || IsInlineAttachment); } }
#if !NETCOREAPP
public FontWeight Weight { get { return Hide ? FontWeights.ExtraLight: FontWeights.SemiBold; } }
#endif
public bool HasContentId { get { return (ContentId != null); } }

// To do: case where ContentLocation property is used instead of ContentId
Expand Down
20 changes: 15 additions & 5 deletions XstFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ public void ReadFolderTree()
foreach (var f in root.Folders)
{
// We may be called on a background thread, so we need to dispatch this to the UI thread
Application.Current.Dispatcher.Invoke(new Action(() =>
DispatchIfNeeded(new Action(() =>
{
view.RootFolders.Add(f);
}));
Expand All @@ -198,7 +198,7 @@ public void ReadMessages(Folder f)
.ToList(); // to force complete execution on the current thread

// We may be called on a background thread, so we need to dispatch this to the UI thread
Application.Current.Dispatcher.Invoke(new Action(() =>
DispatchIfNeeded(new Action(() =>
{
f.Messages.Clear();
foreach (var m in ms)
Expand All @@ -210,6 +210,16 @@ public void ReadMessages(Folder f)
}
}

// We may be called on a background thread, so we need to dispatch this to the UI thread
private void DispatchIfNeeded(Action action)
{
#if !NETCOREAPP
Application.Current.Dispatcher.Invoke(action);
#else
action();
#endif
}

public void ReadMessageDetails(Message m)
{
using (var fs = ndb.GetReadStream())
Expand Down Expand Up @@ -417,9 +427,9 @@ public void ExportMessageProperties(IEnumerable<Message> messages, string fileNa
}
}
}
#endregion
#endregion

#region Private methods
#region Private methods

// Recurse down the folder tree, building a structure of Folder classes
private Folder ReadFolderStructure(FileStream fs, NID nid)
Expand Down Expand Up @@ -523,7 +533,7 @@ private string EnforceCsvValueLengthLimit(string value)
return value.Substring(0, valueLengthLimit) + "…";
}

#endregion
#endregion
}

public class XstException : Exception
Expand Down