Word / Office 365 邮件合并 - SQL 连接字符串的 UDL 文件替代方案
Word / Office 365 Mail Merge - Alternative to UDL file for SQL connection string
我们提供了一个大型软件套件,其中包括在 MS Word / Office 365 中执行邮件合并的功能。
为了让Word知道从哪里获取合并字段列表和数据,我们建立了一个.UDL文件:-
[oledb]
; Everything after this line is an OLE DB initstring
Provider=SQLOLEDB.1;
Data Source=SQLInstanceInPlainText\SQLServerInPlainText;
Initial Catalog=DatabaseCatalogInPlainText;
User ID=UsernameInPlainText;
Password=PasswordInPLainText;
Persist Security Info=True;
如前所述,.UDL 文件中的所有信息都以纯文本形式存储,包括用户名和密码(因为 Microsoft 和安全是两个相互排斥的概念。
到目前为止,这还不是一个主要问题:我们所有的客户都将我们的软件和他们的数据库安装在物理上位于其场所的服务器上,并且无法从外部访问。
所以,有一个安全漏洞,但考虑到必须有人在建筑物内才能利用它,所以这是微不足道的。
但是,我们现在有几家公司(毫无疑问还会有更多公司)希望迁移到基于云的系统。我们已经适当修改了我们的软件,使其也可以完全在 Azure 上运行,并且正在对其进行测试。
在测试期间,我们已经到了需要更新 .UDL 文件以指向 SQL Azure 数据库的地步。
这个数据库当然不在他们的办公场所,而是在云端,因此可以从任何地方访问。就至少一家公司而言,他们希望在家/在现场工作的人可以访问它,并且目前没有设置任何形式的 VPN 来允许我们锁定哪些 IP 地址可以访问它(我们是试图说服他们!)。
因此,如果我们使用 .UDL 文件,我们可能会将这家(毫无疑问还有其他公司)公司置于有人可以泄露数据库连接详细信息的情况下,就这样,游戏结束了。
所以,我正在寻找 .UDL 文件的替代品,以某种方式让我们仍然可以使用 MS Word / Office 365 来执行邮件合并。
具有加密细节的东西至少会有所改进,但对我们来说是一个理想的解决方案 将适用于 MS Word / Office 365 从 .EXE 程序请求详细信息。
None 我们其余的软件都有硬编码的连接字符串(例如在 app.config 中),它们都以高度加密的方式存储在我们的服务器上。当软件启动时,它会从我们的 selected 数据库请求加密的连接详细信息(大多数公司都有几个,例如 Live、Test 和 UAT,他们的软件版本只能看到他们的数据库)服务器。
如果 MS Word / Office 365 可以(也许通过加载项,我已经编写了 Outlook 加载项,所以可以试一试)从程序而不是 . UDL 文件,我们可以应用相同的解决方案并要求用户 select 数据库并登录(使用他们的软件用户名和密码,而不是 SQL 用户名和密码)。
因此,不仅没有人知道 SQL 连接细节,任何离开的用户的软件用户名和密码都将被撤销,因此即使他们仍然拥有该软件(例如在笔记本电脑上)他们无法再访问系统。
同样,如果那台笔记本电脑丢失或被盗,最终得到它的人仍然无法获得详细信息,因为他们无法登录我们的软件。
当然,假设可以通过某种方式让 MS Word / Office 365 从除包含纯文本格式的文件以外的任何内容中获取连接详细信息...
TL;DR...
MS Word / Office 365 使用 .UDL 文件作为 SQL 邮件合并的连接详细信息。
这些详细信息以纯文本形式存储,因此是一个严重的安全漏洞。
将数据库锁定到特定 IP 目前不是一个选项(客户,不是我们!)
我们正在寻找 .UDL 文件的替代方案,最好是 MS Word / Office 365 从 .EXE 程序请求详细信息,但至少要以加密形式存储详细信息。
编辑:
我提到了一个 Word 加载项,如果 Word 可以使用其中一个直接请求连接详细信息(而不是通过单独的 .EXE 程序),那么这也是一个很好的解决方案。我编写的 Outlook 加载项已经这样做了,所以如果有人能指出正确的方向,我可能可以压缩大部分代码。
编辑:尝试加密 ODC 文件的连接字符串
我找到了以下代码,它加密了 web.config 和 app.config
的连接字符串
// Protect the connectionStrings section.
private static void ProtectConfiguration()
{
System.Configuration.Configuration config = ConfigurationManager.
OpenExeConfiguration(ConfigurationUserLevel.None);
// Define the Rsa provider name.
string provider = "RsaProtectedConfigurationProvider";
// Get the section to protect.
ConfigurationSection connStrings = config.ConnectionStrings;
if (connStrings != null)
{
if (!connStrings.SectionInformation.IsProtected)
{
if (!connStrings.ElementInformation.IsLocked)
{
// Protect the section.
connStrings.SectionInformation.ProtectSection(provider);
connStrings.SectionInformation.ForceSave = true;
config.Save(ConfigurationSaveMode.Modified);
Console.WriteLine("Section {0} is now protected by {1}",
connStrings.SectionInformation.Name,
connStrings.SectionInformation.ProtectionProvider.Name);
}
else
Console.WriteLine(
"Can't protect, section {0} is locked",
connStrings.SectionInformation.Name);
}
else
Console.WriteLine(
"Section {0} is already protected by {1}",
connStrings.SectionInformation.Name,
connStrings.SectionInformation.ProtectionProvider.Name);
}
else
Console.WriteLine("Can't get the section {0}",
connStrings.SectionInformation.Name);
}
所以,我写了一个快速程序,将连接字符串加密到它的 app.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<connectionStrings>
<add name="ODC" connectionString="Provider=SQLOLEDB.1;Password=Password;Persist Security Info=True;User ID=Username;Data Source=Server\Instance;Use Procedure for Prepare=1;Auto Translate=True;Packet Size=4096;Use Encryption for Data=False;Tag with column collation when possible=False;Initial Catalog=Database" providerName="System.Data.EntityClient" />
</connectionStrings>
</configuration>
运行 程序,.exe.config 现在包含以下内容
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<connectionStrings configProtectionProvider="RsaProtectedConfigurationProvider">
<EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element"
xmlns="http://www.w3.org/2001/04/xmlenc#">
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" />
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" />
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyName>Rsa Key</KeyName>
</KeyInfo>
<CipherData>
<CipherValue>r40SAb8XRp6w8KLAi+QOZiU9wBDBdQ5Z57QqibCdBTX1KlMXTGorCtjZS1jEzsRt+2qqTb1pRqkC81a8NSbEY0CtuR03nq8Wn8nFp+pEpNnT0fWEvxw9oCAF7HhxcrRao24AbMNzO+RnBIxDtBiCRieQdaQvR6Bp+//LheE8i6Z7MAeTPbKvD2RyFXBxEJ45MopNgGpq511GDaLen9tcaGPwRjO20Hwhoc2po1viqLd/UzEhpFFDrb7ffZm+p5ghUOjcysNHSnbUUJcNnv6z+IemTMytG6Ikr11cACs0NMfXeuA3Ab20btBoBILNq6I+l82p3gXKkNeCz+JV8UmCJA==</CipherValue>
</CipherData>
</EncryptedKey>
</KeyInfo>
<CipherData>
<CipherValue>Bj+N17Mh2Wsdj/gfutYomGo7NctoEHgJyE3NXqfX2+s6jtBTOyNJJihIg5e8elRdf9kJlRP0mrJievFP6LrUZsGoIGE6Z6Ldz7sPE4f1kgdcQEUGBTA2Ir0qnfR05Vk0QL/3MTnTg12BB7U5V742NOQfdrdYqqxC4faFSYlW3ETnlrKWVXLVEijI0ovjq+f3rGBsgbOVCOO+lnkiMDBOJnqklfte9KbkgQ44Kju6buveltINGDNZl2YD7g4RxyyMhkbSfwDvPvO2rr4kZgy843kVwl0sv2LwBErprBtt6gaCxTriH4V1rq02lUVyXDVr4oeynJxQbeRy4Uha2j5U9kk8KuWhi5XL+6aNazvKhWxp+EiliciBVPHffT/1uu3IKkLRD+k4mZMV+bL+1rkufCXno07g3KpfXkA6WhbxI0XayIMj/QY00VkPSSq8dfRu+5tWAz1D3VgHMgC9yHQGwV6TpC/ONnFFLRxKhKIv9nEiUi0MTDdMZfINotnoCYmgu8ylfkvBRSYIITat4ZHiU48B29MXLYYA6KRHMKi/v+o=</CipherValue>
</CipherData>
</EncryptedData>
</connectionStrings>
</configuration>
所以,我抓住它并在我拥有的 ODC 文件中尝试了它,但是 Word 只是说 'Record 1 contained too few data fields',当我单击确定时它说 'Record 2 contained too few data fields',然后重复了几次
下面是工作状态下的ODC文件,新的部分被注释掉了。
<html xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns="http://www.w3.org/TR/REC-html40">
<head>
<meta http-equiv=Content-Type content="text/x-ms-odc; charset=utf-8">
<meta name=ProgId content=ODC.Table>
<meta name=SourceType content=OLEDB>
<meta name=Catalog content="OurServer">
<meta name=Schema content=dbo>
<meta name=Table content="uvw_OurView">
<title>uvw_MMClientDetails</title>
<xml id=docprops><o:DocumentProperties
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns="http://www.w3.org/TR/REC-html40">
<o:Description>Test</o:Description>
<o:Name>uvw_OurView</o:Name>
</o:DocumentProperties>
</xml><xml id=msodc><odc:OfficeDataConnection
xmlns:odc="urn:schemas-microsoft-com:office:odc"
xmlns="http://www.w3.org/TR/REC-html40">
<odc:Connection odc:Type="OLEDB">
<odc:ConnectionString>Provider=SQLOLEDB.1;Password=Password;Persist Security Info=True;User ID=Username;Data Source=Server\Instance;Use Procedure for Prepare=1;Auto Translate=True;Packet Size=4096;Use Encryption for Data=False;Tag with column collation when possible=False;Initial Catalog=Database</odc:ConnectionString>
<!--
<connectionStrings configProtectionProvider="RsaProtectedConfigurationProvider">
<EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element"
xmlns="http://www.w3.org/2001/04/xmlenc#">
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" />
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" />
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyName>Rsa Key</KeyName>
</KeyInfo>
<CipherData>
<CipherValue>r40SAb8XRp6w8KLAi+QOZiU9wBDBdQ5Z57QqibCdBTX1KlMXTGorCtjZS1jEzsRt+2qqTb1pRqkC81a8NSbEY0CtuR03nq8Wn8nFp+pEpNnT0fWEvxw9oCAF7HhxcrRao24AbMNzO+RnBIxDtBiCRieQdaQvR6Bp+//LheE8i6Z7MAeTPbKvD2RyFXBxEJ45MopNgGpq511GDaLen9tcaGPwRjO20Hwhoc2po1viqLd/UzEhpFFDrb7ffZm+p5ghUOjcysNHSnbUUJcNnv6z+IemTMytG6Ikr11cACs0NMfXeuA3Ab20btBoBILNq6I+l82p3gXKkNeCz+JV8UmCJA==</CipherValue>
</CipherData>
</EncryptedKey>
</KeyInfo>
<CipherData>
<CipherValue>Bj+N17Mh2Wsdj/gfutYomGo7NctoEHgJyE3NXqfX2+s6jtBTOyNJJihIg5e8elRdf9kJlRP0mrJievFP6LrUZsGoIGE6Z6Ldz7sPE4f1kgdcQEUGBTA2Ir0qnfR05Vk0QL/3MTnTg12BB7U5V742NOQfdrdYqqxC4faFSYlW3ETnlrKWVXLVEijI0ovjq+f3rGBsgbOVCOO+lnkiMDBOJnqklfte9KbkgQ44Kju6buveltINGDNZl2YD7g4RxyyMhkbSfwDvPvO2rr4kZgy843kVwl0sv2LwBErprBtt6gaCxTriH4V1rq02lUVyXDVr4oeynJxQbeRy4Uha2j5U9kk8KuWhi5XL+6aNazvKhWxp+EiliciBVPHffT/1uu3IKkLRD+k4mZMV+bL+1rkufCXno07g3KpfXkA6WhbxI0XayIMj/QY00VkPSSq8dfRu+5tWAz1D3VgHMgC9yHQGwV6TpC/ONnFFLRxKhKIv9nEiUi0MTDdMZfINotnoCYmgu8ylfkvBRSYIITat4ZHiU48B29MXLYYA6KRHMKi/v+o=</CipherValue>
</CipherData>
</EncryptedData>
</connectionStrings>
-->
<odc:CommandType>Table</odc:CommandType>
<odc:CommandText>"Database"."dbo"."uvw_OurView"</odc:CommandText>
</odc:Connection>
</odc:OfficeDataConnection>
</xml>
<style>
<!--
.ODCDataSource
{
behavior: url(dataconn.htc);
}
-->
</style>
</head>
<body onload='init()' scroll=no leftmargin=0 topmargin=0 rightmargin=0 style='border: 0px'>
<table style='border: solid 1px threedface; height: 100%; width: 100%' cellpadding=0 cellspacing=0 width='100%'>
<tr>
<td id=tdName style='font-family:arial; font-size:medium; padding: 3px; background-color: threedface'>
</td>
<td id=tdTableDropdown style='padding: 3px; background-color: threedface; vertical-align: top; padding-bottom: 3px'>
</td>
</tr>
<tr>
<td id=tdDesc colspan='2' style='border-bottom: 1px threedshadow solid; font-family: Arial; font-size: 1pt; padding: 2px; background-color: threedface'>
</td>
</tr>
<tr>
<td colspan='2' style='height: 100%; padding-bottom: 4px; border-top: 1px threedhighlight solid;'>
<div id='pt' style='height: 100%' class='ODCDataSource'></div>
</td>
</tr>
</table>
<script language='javascript'>
function init() {
var sName, sDescription;
var i, j;
try {
sName = unescape(location.href)
i = sName.lastIndexOf(".")
if (i>=0) { sName = sName.substring(1, i); }
i = sName.lastIndexOf("/")
if (i>=0) { sName = sName.substring(i+1, sName.length); }
document.title = sName;
document.getElementById("tdName").innerText = sName;
sDescription = document.getElementById("docprops").innerHTML;
i = sDescription.indexOf("escription>")
if (i>=0) { j = sDescription.indexOf("escription>", i + 11); }
if (i>=0 && j >= 0) {
j = sDescription.lastIndexOf("</", j);
if (j>=0) {
sDescription = sDescription.substring(i+11, j);
if (sDescription != "") {
document.getElementById("tdDesc").style.fontSize="x-small";
document.getElementById("tdDesc").innerHTML = sDescription;
}
}
}
}
catch(e) {
}
}
</script>
</body>
</html>
我尝试使用连接字符串作为连接字符串,称为 connectionStrings,并且与原始连接字符串一样,称为 odc:ConnectionString,但均无效。
任何人都知道我是否在正确的轨道上,只需要调整一些东西,或者 ODC 文件/Word/Office 365 不能处理加密和 configProtectionProvider="RsaProtectedConfigurationProvider"?
我会把它写成评论,但没有要点。
我所知道的每一种方法都有一个严重的问题:在 Word 建立连接后,无论是通过 UI 还是通过 OpenDataSource,完整的连接字符串,包括(在连接到SQL服务器)的用户名和密码,保存在文件中。
在 .docx 文件中,它存储在
中的 settings.xml 中
<w:settings><w:mailMerge><w:odso><w:udl> .
我认为您无法避免这种情况,当然这意味着任何可以保存文档的未加密副本的人都可以找到相关的连接信息。我想您一定已经看到,除非您在连接时指定 "saving the security information",否则 Word 不会完成连接。
换句话说,您所做的任何涉及到 SQL 服务器的直接连接都必须以某种方式处理该问题。
顺便说一句,您总是必须提供一种或另一种外部对象才能连接到 SQL 服务器 - 机器或文件 ODBC DSN、.udl 文件或 . .odc 文件。我不认为有一种方法可以加密 Word 可以使用的任何一个。
您过去可以提供一个完全空的 .odc 文件并提供连接信息。在 OpenDataSource 调用的 ConnectString 参数中。如果有什么可以让您解决问题,那就是按照这些思路,AFAIK 它仍然遇到我上面描述的问题,但也许有一个解决方案。
如果真的没有解决方法,我认为您有两种可能的方法:
一种。不要使用邮件合并。 (例如,编写您自己的合并代码)
b.看看有没有办法间接获取数据
至于 (b),您也许可以创建一个 Jet 数据库,linked 到您的 SQL 服务器数据,并将其用作您的数据源。这不是我深入探讨的问题,但 Jet 似乎遇到与 Word 相同的问题,即最终存储安全信息。可以检查,或者坚持使用自己的对话框来提示安全信息。这也不适合你。
或者,如果您被允许在本地存储数据,您可以从您的 SQL 服务器中填充 Jet 数据库并使用它。
Jet 可能是最可靠的中间格式,但如果您需要超过 255/256 列,则必须使用其他格式。
另一种间接获取数据的方法是编写您自己的 OLEDB provider/ODBC 驱动程序或 Word 文件转换器,并从中建立连接,然后分发该组件。
麻烦的是 none 这些东西很简单。例如,尽管 Microsoft 曾经提供 "Simple OLEDB Provider" 让您通过实现一些简单的接口来创建提供者,但是生成的提供者并没有实现 Word 需要能够使用的 OLE DB 命令接口。因此,您可能必须编写一个更完整的 OLE DB 提供程序或 ODBC 驱动程序,或者许可其中一个第 3 方工具包来执行此操作。
[[转换器方法更加离奇,但在这一点上我不妨概述一下。它依赖于滥用 Word 的一种文本文件转换技术。不是使用 .NET/COM 的较新的(那些转换器不能用于连接到数据源),而是通过 link here
的更旧的
你的想法是编写一个转换器,它可以打开和识别具有特定文件扩展名(比如“.sqs”)的文本文件,然后将该文件中的文本块转换为 RTF 格式并提供它到 Word。
因此,要在这种情况下使用它,您需要提供一个合适的 .sqs 文件(可能没有任何内容,或者可能包含一些帮助您连接到正确数据源的信息)。该文件必须存在。但是,当 Word 请求数据时,您的代码并没有实际转换该文件的文本,而是读取您的数据库数据,并 returns 改为 RTF 格式。]]
我分两部分解决了这个问题
1) 邮件合并
实际的邮件合并本身是由使用 Spire.PDF 库的主 C# 程序执行的。这样就无需使用 UDL 或 ODC 文件来将数据库中的数据导入文档。
2) 模板文件
这仍然留下了使用视图中的合并字段创建邮件合并模板文件的问题。这必须可以从主 C# 程序外部完成,并且以前需要一个 UDL 文件。
我根据插件公司网站上的博客代码编写了一个 Word 插件来解决这个问题:https://www.add-in-express.com/creating-addins-blog/2013/07/05/automating-word-mail-merge/
此加载项向 'Add-Ins' 功能区添加了一个部分,名为“[我们的产品] 邮件合并”,带有一个按钮 'Add Merge Fields'。单击该按钮将打开一个新表单。
注意:这是测试一切正常的早期版本,所以不是特别漂亮,缺少一些错误检查等
using System;
using System.Data;
using System.Data.SqlClient;
using System.Windows.Forms;
using Word = Microsoft.Office.Interop.Word;
using System.Runtime.InteropServices;
namespace RPMWordMailMergeAddIn
{
public partial class MailMergeForm : Form
{
private static string connectionString = null;
public MailMergeForm()
{
InitializeComponent();
if (connectionString != null)
{
btnConnect_Click(null, null);
}
}
private void btnConnect_Click(object sender, EventArgs e)
{
if (connectionString == null || sender != null)
{
connectionString = <Call our 'Login to the Software' screen,
and return the restricted database user connection details>
tvMailMergeTables.Nodes.Clear();
}
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
var views = connection.GetSchema("Views");
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
builder.ConnectionString = connectionString;
TreeNode rootNode = new TreeNode(builder.InitialCatalog, 1, 1);
rootNode.Tag = "Server";
TreeNode viewsNode = rootNode.Nodes.Add("Views", "Views", 2, 2);
foreach (DataRow view in views.Rows)
{
var viewName = view.ItemArray[2].ToString();
if (viewName.StartsWith("uvw_MM") && !viewName.EndsWith("_X"))
{
TreeNode tableNode = viewsNode.Nodes.Add(viewName, viewName, 3, 3);
tableNode.Tag = "View";
var schemaOptions = new string[4];
schemaOptions[2] = viewName;
var columns = connection.GetSchema("Columns", schemaOptions);
foreach (DataRow column in columns.Rows)
{
var columnName = column.ItemArray[3].ToString();
TreeNode columnNode = tableNode.Nodes.Add(columnName, columnName, 6, 6);
columnNode.Tag = "Column";
}
}
}
tvMailMergeTables.Nodes.Add(rootNode);
rootNode.Expand();
viewsNode.Expand();
connection.Close();
}
}
private void tvMailMergeTables_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
{
TreeNode selectedNode = e.Node;
if (selectedNode.Tag.ToString() == "Column" && selectedNode.IsSelected) // Fix for TreeView expanding bug
{
Word.Application wordApp = null;
Word.Document doc = null;
Word.Selection selection = null;
Word.MailMerge wordMerge = null;
Word.MailMergeFields wordMergeFields = null;
try
{
wordApp = ThisAddIn.application;
doc = wordApp.ActiveDocument;
wordMerge = doc.MailMerge;
wordMergeFields = wordMerge.Fields;
selection = wordApp.Selection;
wordMergeFields.Add(selection.Range, selectedNode.Text);
}
finally
{
if (wordMergeFields != null)
{
Marshal.ReleaseComObject(wordMergeFields);
}
if (wordMerge != null)
{
Marshal.ReleaseComObject(wordMerge);
}
if (selection != null)
{
Marshal.ReleaseComObject(selection);
}
if (doc != null)
{
Marshal.ReleaseComObject(doc);
}
}
}
}
}
}
此表单然后要求用户登录到我们的软件,如果他们成功登录,它 returns 只读数据库用户的连接字符串,该用户只能访问邮件合并视图.
然后用视图及其列填充 TreeView 控件。
双击列名称会将其添加到 Word 文档中作为当前光标位置的合并域。
保存时,Word 文档不会存储任何连接详细信息。
所以:-
- 不需要 UDL 或 ODC 文件
- 需要有效的软件登录才能查看合并字段
- 合并字段列表总是最新的,因为它每次都会刷新
- 模板或最终邮件合并文档中未保存连接详细信息
正是我所追求的!
额外
此解决方案不再需要,但为了完整起见,也可以从加载项中执行邮件合并。
这需要一个 ODC 文件,但您可以将连接字符串部分留空并改为将连接字符串传递给它,从而避免以纯文本形式存储详细信息。
private void btnPerformMailMerge_Click(object sender, EventArgs e)
{
Word.Application wordApp = null;
Word.Document doc = null;
Word.MailMerge wordMerge = null;
Word.MailMergeFields wordMergeFields = null;
object sqlQuery = string.Empty;
object connection = string.Empty;
string odcPath = string.Empty;
Object oMissing = System.Reflection.Missing.Value;
Object oFalse = false;
try
{
wordApp = ThisAddIn.application;
doc = wordApp.ActiveDocument;
wordMerge = doc.MailMerge;
wordMergeFields = wordMerge.Fields;
connection = connectionString;
sqlQuery = String.Format("Select * From \"{0}\"", viewName);
var name = @"Empty.odc";
wordMerge.OpenDataSource(name, ref oMissing,
ref oMissing, ref oMissing, ref oMissing,
ref oMissing, ref oMissing, ref oMissing,
ref oMissing, ref oMissing, ref oMissing,
ref connection, ref sqlQuery, ref oMissing,
ref oMissing, ref oMissing);
wordMerge.Destination = Word.WdMailMergeDestination.wdSendToNewDocument;
wordMerge.Execute(ref oFalse);
}
finally
{
if (wordMergeFields != null)
{
Marshal.ReleaseComObject(wordMergeFields);
}
if (wordMerge != null)
{
Marshal.ReleaseComObject(wordMerge);
}
if (doc != null)
{
Marshal.ReleaseComObject(doc);
}
}
}
我们提供了一个大型软件套件,其中包括在 MS Word / Office 365 中执行邮件合并的功能。
为了让Word知道从哪里获取合并字段列表和数据,我们建立了一个.UDL文件:-
[oledb]
; Everything after this line is an OLE DB initstring
Provider=SQLOLEDB.1;
Data Source=SQLInstanceInPlainText\SQLServerInPlainText;
Initial Catalog=DatabaseCatalogInPlainText;
User ID=UsernameInPlainText;
Password=PasswordInPLainText;
Persist Security Info=True;
如前所述,.UDL 文件中的所有信息都以纯文本形式存储,包括用户名和密码(因为 Microsoft 和安全是两个相互排斥的概念。
到目前为止,这还不是一个主要问题:我们所有的客户都将我们的软件和他们的数据库安装在物理上位于其场所的服务器上,并且无法从外部访问。
所以,有一个安全漏洞,但考虑到必须有人在建筑物内才能利用它,所以这是微不足道的。
但是,我们现在有几家公司(毫无疑问还会有更多公司)希望迁移到基于云的系统。我们已经适当修改了我们的软件,使其也可以完全在 Azure 上运行,并且正在对其进行测试。
在测试期间,我们已经到了需要更新 .UDL 文件以指向 SQL Azure 数据库的地步。
这个数据库当然不在他们的办公场所,而是在云端,因此可以从任何地方访问。就至少一家公司而言,他们希望在家/在现场工作的人可以访问它,并且目前没有设置任何形式的 VPN 来允许我们锁定哪些 IP 地址可以访问它(我们是试图说服他们!)。
因此,如果我们使用 .UDL 文件,我们可能会将这家(毫无疑问还有其他公司)公司置于有人可以泄露数据库连接详细信息的情况下,就这样,游戏结束了。
所以,我正在寻找 .UDL 文件的替代品,以某种方式让我们仍然可以使用 MS Word / Office 365 来执行邮件合并。
具有加密细节的东西至少会有所改进,但对我们来说是一个理想的解决方案 将适用于 MS Word / Office 365 从 .EXE 程序请求详细信息。
None 我们其余的软件都有硬编码的连接字符串(例如在 app.config 中),它们都以高度加密的方式存储在我们的服务器上。当软件启动时,它会从我们的 selected 数据库请求加密的连接详细信息(大多数公司都有几个,例如 Live、Test 和 UAT,他们的软件版本只能看到他们的数据库)服务器。
如果 MS Word / Office 365 可以(也许通过加载项,我已经编写了 Outlook 加载项,所以可以试一试)从程序而不是 . UDL 文件,我们可以应用相同的解决方案并要求用户 select 数据库并登录(使用他们的软件用户名和密码,而不是 SQL 用户名和密码)。
因此,不仅没有人知道 SQL 连接细节,任何离开的用户的软件用户名和密码都将被撤销,因此即使他们仍然拥有该软件(例如在笔记本电脑上)他们无法再访问系统。
同样,如果那台笔记本电脑丢失或被盗,最终得到它的人仍然无法获得详细信息,因为他们无法登录我们的软件。
当然,假设可以通过某种方式让 MS Word / Office 365 从除包含纯文本格式的文件以外的任何内容中获取连接详细信息...
TL;DR...
MS Word / Office 365 使用 .UDL 文件作为 SQL 邮件合并的连接详细信息。
这些详细信息以纯文本形式存储,因此是一个严重的安全漏洞。
将数据库锁定到特定 IP 目前不是一个选项(客户,不是我们!)
我们正在寻找 .UDL 文件的替代方案,最好是 MS Word / Office 365 从 .EXE 程序请求详细信息,但至少要以加密形式存储详细信息。
编辑:
我提到了一个 Word 加载项,如果 Word 可以使用其中一个直接请求连接详细信息(而不是通过单独的 .EXE 程序),那么这也是一个很好的解决方案。我编写的 Outlook 加载项已经这样做了,所以如果有人能指出正确的方向,我可能可以压缩大部分代码。
编辑:尝试加密 ODC 文件的连接字符串
我找到了以下代码,它加密了 web.config 和 app.config
的连接字符串// Protect the connectionStrings section.
private static void ProtectConfiguration()
{
System.Configuration.Configuration config = ConfigurationManager.
OpenExeConfiguration(ConfigurationUserLevel.None);
// Define the Rsa provider name.
string provider = "RsaProtectedConfigurationProvider";
// Get the section to protect.
ConfigurationSection connStrings = config.ConnectionStrings;
if (connStrings != null)
{
if (!connStrings.SectionInformation.IsProtected)
{
if (!connStrings.ElementInformation.IsLocked)
{
// Protect the section.
connStrings.SectionInformation.ProtectSection(provider);
connStrings.SectionInformation.ForceSave = true;
config.Save(ConfigurationSaveMode.Modified);
Console.WriteLine("Section {0} is now protected by {1}",
connStrings.SectionInformation.Name,
connStrings.SectionInformation.ProtectionProvider.Name);
}
else
Console.WriteLine(
"Can't protect, section {0} is locked",
connStrings.SectionInformation.Name);
}
else
Console.WriteLine(
"Section {0} is already protected by {1}",
connStrings.SectionInformation.Name,
connStrings.SectionInformation.ProtectionProvider.Name);
}
else
Console.WriteLine("Can't get the section {0}",
connStrings.SectionInformation.Name);
}
所以,我写了一个快速程序,将连接字符串加密到它的 app.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<connectionStrings>
<add name="ODC" connectionString="Provider=SQLOLEDB.1;Password=Password;Persist Security Info=True;User ID=Username;Data Source=Server\Instance;Use Procedure for Prepare=1;Auto Translate=True;Packet Size=4096;Use Encryption for Data=False;Tag with column collation when possible=False;Initial Catalog=Database" providerName="System.Data.EntityClient" />
</connectionStrings>
</configuration>
运行 程序,.exe.config 现在包含以下内容
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<connectionStrings configProtectionProvider="RsaProtectedConfigurationProvider">
<EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element"
xmlns="http://www.w3.org/2001/04/xmlenc#">
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" />
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" />
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyName>Rsa Key</KeyName>
</KeyInfo>
<CipherData>
<CipherValue>r40SAb8XRp6w8KLAi+QOZiU9wBDBdQ5Z57QqibCdBTX1KlMXTGorCtjZS1jEzsRt+2qqTb1pRqkC81a8NSbEY0CtuR03nq8Wn8nFp+pEpNnT0fWEvxw9oCAF7HhxcrRao24AbMNzO+RnBIxDtBiCRieQdaQvR6Bp+//LheE8i6Z7MAeTPbKvD2RyFXBxEJ45MopNgGpq511GDaLen9tcaGPwRjO20Hwhoc2po1viqLd/UzEhpFFDrb7ffZm+p5ghUOjcysNHSnbUUJcNnv6z+IemTMytG6Ikr11cACs0NMfXeuA3Ab20btBoBILNq6I+l82p3gXKkNeCz+JV8UmCJA==</CipherValue>
</CipherData>
</EncryptedKey>
</KeyInfo>
<CipherData>
<CipherValue>Bj+N17Mh2Wsdj/gfutYomGo7NctoEHgJyE3NXqfX2+s6jtBTOyNJJihIg5e8elRdf9kJlRP0mrJievFP6LrUZsGoIGE6Z6Ldz7sPE4f1kgdcQEUGBTA2Ir0qnfR05Vk0QL/3MTnTg12BB7U5V742NOQfdrdYqqxC4faFSYlW3ETnlrKWVXLVEijI0ovjq+f3rGBsgbOVCOO+lnkiMDBOJnqklfte9KbkgQ44Kju6buveltINGDNZl2YD7g4RxyyMhkbSfwDvPvO2rr4kZgy843kVwl0sv2LwBErprBtt6gaCxTriH4V1rq02lUVyXDVr4oeynJxQbeRy4Uha2j5U9kk8KuWhi5XL+6aNazvKhWxp+EiliciBVPHffT/1uu3IKkLRD+k4mZMV+bL+1rkufCXno07g3KpfXkA6WhbxI0XayIMj/QY00VkPSSq8dfRu+5tWAz1D3VgHMgC9yHQGwV6TpC/ONnFFLRxKhKIv9nEiUi0MTDdMZfINotnoCYmgu8ylfkvBRSYIITat4ZHiU48B29MXLYYA6KRHMKi/v+o=</CipherValue>
</CipherData>
</EncryptedData>
</connectionStrings>
</configuration>
所以,我抓住它并在我拥有的 ODC 文件中尝试了它,但是 Word 只是说 'Record 1 contained too few data fields',当我单击确定时它说 'Record 2 contained too few data fields',然后重复了几次
下面是工作状态下的ODC文件,新的部分被注释掉了。
<html xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns="http://www.w3.org/TR/REC-html40">
<head>
<meta http-equiv=Content-Type content="text/x-ms-odc; charset=utf-8">
<meta name=ProgId content=ODC.Table>
<meta name=SourceType content=OLEDB>
<meta name=Catalog content="OurServer">
<meta name=Schema content=dbo>
<meta name=Table content="uvw_OurView">
<title>uvw_MMClientDetails</title>
<xml id=docprops><o:DocumentProperties
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns="http://www.w3.org/TR/REC-html40">
<o:Description>Test</o:Description>
<o:Name>uvw_OurView</o:Name>
</o:DocumentProperties>
</xml><xml id=msodc><odc:OfficeDataConnection
xmlns:odc="urn:schemas-microsoft-com:office:odc"
xmlns="http://www.w3.org/TR/REC-html40">
<odc:Connection odc:Type="OLEDB">
<odc:ConnectionString>Provider=SQLOLEDB.1;Password=Password;Persist Security Info=True;User ID=Username;Data Source=Server\Instance;Use Procedure for Prepare=1;Auto Translate=True;Packet Size=4096;Use Encryption for Data=False;Tag with column collation when possible=False;Initial Catalog=Database</odc:ConnectionString>
<!--
<connectionStrings configProtectionProvider="RsaProtectedConfigurationProvider">
<EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element"
xmlns="http://www.w3.org/2001/04/xmlenc#">
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" />
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" />
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyName>Rsa Key</KeyName>
</KeyInfo>
<CipherData>
<CipherValue>r40SAb8XRp6w8KLAi+QOZiU9wBDBdQ5Z57QqibCdBTX1KlMXTGorCtjZS1jEzsRt+2qqTb1pRqkC81a8NSbEY0CtuR03nq8Wn8nFp+pEpNnT0fWEvxw9oCAF7HhxcrRao24AbMNzO+RnBIxDtBiCRieQdaQvR6Bp+//LheE8i6Z7MAeTPbKvD2RyFXBxEJ45MopNgGpq511GDaLen9tcaGPwRjO20Hwhoc2po1viqLd/UzEhpFFDrb7ffZm+p5ghUOjcysNHSnbUUJcNnv6z+IemTMytG6Ikr11cACs0NMfXeuA3Ab20btBoBILNq6I+l82p3gXKkNeCz+JV8UmCJA==</CipherValue>
</CipherData>
</EncryptedKey>
</KeyInfo>
<CipherData>
<CipherValue>Bj+N17Mh2Wsdj/gfutYomGo7NctoEHgJyE3NXqfX2+s6jtBTOyNJJihIg5e8elRdf9kJlRP0mrJievFP6LrUZsGoIGE6Z6Ldz7sPE4f1kgdcQEUGBTA2Ir0qnfR05Vk0QL/3MTnTg12BB7U5V742NOQfdrdYqqxC4faFSYlW3ETnlrKWVXLVEijI0ovjq+f3rGBsgbOVCOO+lnkiMDBOJnqklfte9KbkgQ44Kju6buveltINGDNZl2YD7g4RxyyMhkbSfwDvPvO2rr4kZgy843kVwl0sv2LwBErprBtt6gaCxTriH4V1rq02lUVyXDVr4oeynJxQbeRy4Uha2j5U9kk8KuWhi5XL+6aNazvKhWxp+EiliciBVPHffT/1uu3IKkLRD+k4mZMV+bL+1rkufCXno07g3KpfXkA6WhbxI0XayIMj/QY00VkPSSq8dfRu+5tWAz1D3VgHMgC9yHQGwV6TpC/ONnFFLRxKhKIv9nEiUi0MTDdMZfINotnoCYmgu8ylfkvBRSYIITat4ZHiU48B29MXLYYA6KRHMKi/v+o=</CipherValue>
</CipherData>
</EncryptedData>
</connectionStrings>
-->
<odc:CommandType>Table</odc:CommandType>
<odc:CommandText>"Database"."dbo"."uvw_OurView"</odc:CommandText>
</odc:Connection>
</odc:OfficeDataConnection>
</xml>
<style>
<!--
.ODCDataSource
{
behavior: url(dataconn.htc);
}
-->
</style>
</head>
<body onload='init()' scroll=no leftmargin=0 topmargin=0 rightmargin=0 style='border: 0px'>
<table style='border: solid 1px threedface; height: 100%; width: 100%' cellpadding=0 cellspacing=0 width='100%'>
<tr>
<td id=tdName style='font-family:arial; font-size:medium; padding: 3px; background-color: threedface'>
</td>
<td id=tdTableDropdown style='padding: 3px; background-color: threedface; vertical-align: top; padding-bottom: 3px'>
</td>
</tr>
<tr>
<td id=tdDesc colspan='2' style='border-bottom: 1px threedshadow solid; font-family: Arial; font-size: 1pt; padding: 2px; background-color: threedface'>
</td>
</tr>
<tr>
<td colspan='2' style='height: 100%; padding-bottom: 4px; border-top: 1px threedhighlight solid;'>
<div id='pt' style='height: 100%' class='ODCDataSource'></div>
</td>
</tr>
</table>
<script language='javascript'>
function init() {
var sName, sDescription;
var i, j;
try {
sName = unescape(location.href)
i = sName.lastIndexOf(".")
if (i>=0) { sName = sName.substring(1, i); }
i = sName.lastIndexOf("/")
if (i>=0) { sName = sName.substring(i+1, sName.length); }
document.title = sName;
document.getElementById("tdName").innerText = sName;
sDescription = document.getElementById("docprops").innerHTML;
i = sDescription.indexOf("escription>")
if (i>=0) { j = sDescription.indexOf("escription>", i + 11); }
if (i>=0 && j >= 0) {
j = sDescription.lastIndexOf("</", j);
if (j>=0) {
sDescription = sDescription.substring(i+11, j);
if (sDescription != "") {
document.getElementById("tdDesc").style.fontSize="x-small";
document.getElementById("tdDesc").innerHTML = sDescription;
}
}
}
}
catch(e) {
}
}
</script>
</body>
</html>
我尝试使用连接字符串作为连接字符串,称为 connectionStrings,并且与原始连接字符串一样,称为 odc:ConnectionString,但均无效。
任何人都知道我是否在正确的轨道上,只需要调整一些东西,或者 ODC 文件/Word/Office 365 不能处理加密和 configProtectionProvider="RsaProtectedConfigurationProvider"?
我会把它写成评论,但没有要点。
我所知道的每一种方法都有一个严重的问题:在 Word 建立连接后,无论是通过 UI 还是通过 OpenDataSource,完整的连接字符串,包括(在连接到SQL服务器)的用户名和密码,保存在文件中。
在 .docx 文件中,它存储在
中的 settings.xml 中<w:settings><w:mailMerge><w:odso><w:udl> .
我认为您无法避免这种情况,当然这意味着任何可以保存文档的未加密副本的人都可以找到相关的连接信息。我想您一定已经看到,除非您在连接时指定 "saving the security information",否则 Word 不会完成连接。
换句话说,您所做的任何涉及到 SQL 服务器的直接连接都必须以某种方式处理该问题。
顺便说一句,您总是必须提供一种或另一种外部对象才能连接到 SQL 服务器 - 机器或文件 ODBC DSN、.udl 文件或 . .odc 文件。我不认为有一种方法可以加密 Word 可以使用的任何一个。
您过去可以提供一个完全空的 .odc 文件并提供连接信息。在 OpenDataSource 调用的 ConnectString 参数中。如果有什么可以让您解决问题,那就是按照这些思路,AFAIK 它仍然遇到我上面描述的问题,但也许有一个解决方案。
如果真的没有解决方法,我认为您有两种可能的方法: 一种。不要使用邮件合并。 (例如,编写您自己的合并代码) b.看看有没有办法间接获取数据
至于 (b),您也许可以创建一个 Jet 数据库,linked 到您的 SQL 服务器数据,并将其用作您的数据源。这不是我深入探讨的问题,但 Jet 似乎遇到与 Word 相同的问题,即最终存储安全信息。可以检查,或者坚持使用自己的对话框来提示安全信息。这也不适合你。
或者,如果您被允许在本地存储数据,您可以从您的 SQL 服务器中填充 Jet 数据库并使用它。
Jet 可能是最可靠的中间格式,但如果您需要超过 255/256 列,则必须使用其他格式。
另一种间接获取数据的方法是编写您自己的 OLEDB provider/ODBC 驱动程序或 Word 文件转换器,并从中建立连接,然后分发该组件。
麻烦的是 none 这些东西很简单。例如,尽管 Microsoft 曾经提供 "Simple OLEDB Provider" 让您通过实现一些简单的接口来创建提供者,但是生成的提供者并没有实现 Word 需要能够使用的 OLE DB 命令接口。因此,您可能必须编写一个更完整的 OLE DB 提供程序或 ODBC 驱动程序,或者许可其中一个第 3 方工具包来执行此操作。
[[转换器方法更加离奇,但在这一点上我不妨概述一下。它依赖于滥用 Word 的一种文本文件转换技术。不是使用 .NET/COM 的较新的(那些转换器不能用于连接到数据源),而是通过 link here
的更旧的你的想法是编写一个转换器,它可以打开和识别具有特定文件扩展名(比如“.sqs”)的文本文件,然后将该文件中的文本块转换为 RTF 格式并提供它到 Word。
因此,要在这种情况下使用它,您需要提供一个合适的 .sqs 文件(可能没有任何内容,或者可能包含一些帮助您连接到正确数据源的信息)。该文件必须存在。但是,当 Word 请求数据时,您的代码并没有实际转换该文件的文本,而是读取您的数据库数据,并 returns 改为 RTF 格式。]]
我分两部分解决了这个问题
1) 邮件合并
实际的邮件合并本身是由使用 Spire.PDF 库的主 C# 程序执行的。这样就无需使用 UDL 或 ODC 文件来将数据库中的数据导入文档。
2) 模板文件
这仍然留下了使用视图中的合并字段创建邮件合并模板文件的问题。这必须可以从主 C# 程序外部完成,并且以前需要一个 UDL 文件。
我根据插件公司网站上的博客代码编写了一个 Word 插件来解决这个问题:https://www.add-in-express.com/creating-addins-blog/2013/07/05/automating-word-mail-merge/
此加载项向 'Add-Ins' 功能区添加了一个部分,名为“[我们的产品] 邮件合并”,带有一个按钮 'Add Merge Fields'。单击该按钮将打开一个新表单。
注意:这是测试一切正常的早期版本,所以不是特别漂亮,缺少一些错误检查等
using System;
using System.Data;
using System.Data.SqlClient;
using System.Windows.Forms;
using Word = Microsoft.Office.Interop.Word;
using System.Runtime.InteropServices;
namespace RPMWordMailMergeAddIn
{
public partial class MailMergeForm : Form
{
private static string connectionString = null;
public MailMergeForm()
{
InitializeComponent();
if (connectionString != null)
{
btnConnect_Click(null, null);
}
}
private void btnConnect_Click(object sender, EventArgs e)
{
if (connectionString == null || sender != null)
{
connectionString = <Call our 'Login to the Software' screen,
and return the restricted database user connection details>
tvMailMergeTables.Nodes.Clear();
}
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
var views = connection.GetSchema("Views");
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
builder.ConnectionString = connectionString;
TreeNode rootNode = new TreeNode(builder.InitialCatalog, 1, 1);
rootNode.Tag = "Server";
TreeNode viewsNode = rootNode.Nodes.Add("Views", "Views", 2, 2);
foreach (DataRow view in views.Rows)
{
var viewName = view.ItemArray[2].ToString();
if (viewName.StartsWith("uvw_MM") && !viewName.EndsWith("_X"))
{
TreeNode tableNode = viewsNode.Nodes.Add(viewName, viewName, 3, 3);
tableNode.Tag = "View";
var schemaOptions = new string[4];
schemaOptions[2] = viewName;
var columns = connection.GetSchema("Columns", schemaOptions);
foreach (DataRow column in columns.Rows)
{
var columnName = column.ItemArray[3].ToString();
TreeNode columnNode = tableNode.Nodes.Add(columnName, columnName, 6, 6);
columnNode.Tag = "Column";
}
}
}
tvMailMergeTables.Nodes.Add(rootNode);
rootNode.Expand();
viewsNode.Expand();
connection.Close();
}
}
private void tvMailMergeTables_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
{
TreeNode selectedNode = e.Node;
if (selectedNode.Tag.ToString() == "Column" && selectedNode.IsSelected) // Fix for TreeView expanding bug
{
Word.Application wordApp = null;
Word.Document doc = null;
Word.Selection selection = null;
Word.MailMerge wordMerge = null;
Word.MailMergeFields wordMergeFields = null;
try
{
wordApp = ThisAddIn.application;
doc = wordApp.ActiveDocument;
wordMerge = doc.MailMerge;
wordMergeFields = wordMerge.Fields;
selection = wordApp.Selection;
wordMergeFields.Add(selection.Range, selectedNode.Text);
}
finally
{
if (wordMergeFields != null)
{
Marshal.ReleaseComObject(wordMergeFields);
}
if (wordMerge != null)
{
Marshal.ReleaseComObject(wordMerge);
}
if (selection != null)
{
Marshal.ReleaseComObject(selection);
}
if (doc != null)
{
Marshal.ReleaseComObject(doc);
}
}
}
}
}
}
此表单然后要求用户登录到我们的软件,如果他们成功登录,它 returns 只读数据库用户的连接字符串,该用户只能访问邮件合并视图.
然后用视图及其列填充 TreeView 控件。
双击列名称会将其添加到 Word 文档中作为当前光标位置的合并域。
保存时,Word 文档不会存储任何连接详细信息。
所以:-
- 不需要 UDL 或 ODC 文件
- 需要有效的软件登录才能查看合并字段
- 合并字段列表总是最新的,因为它每次都会刷新
- 模板或最终邮件合并文档中未保存连接详细信息
正是我所追求的!
额外
此解决方案不再需要,但为了完整起见,也可以从加载项中执行邮件合并。
这需要一个 ODC 文件,但您可以将连接字符串部分留空并改为将连接字符串传递给它,从而避免以纯文本形式存储详细信息。
private void btnPerformMailMerge_Click(object sender, EventArgs e)
{
Word.Application wordApp = null;
Word.Document doc = null;
Word.MailMerge wordMerge = null;
Word.MailMergeFields wordMergeFields = null;
object sqlQuery = string.Empty;
object connection = string.Empty;
string odcPath = string.Empty;
Object oMissing = System.Reflection.Missing.Value;
Object oFalse = false;
try
{
wordApp = ThisAddIn.application;
doc = wordApp.ActiveDocument;
wordMerge = doc.MailMerge;
wordMergeFields = wordMerge.Fields;
connection = connectionString;
sqlQuery = String.Format("Select * From \"{0}\"", viewName);
var name = @"Empty.odc";
wordMerge.OpenDataSource(name, ref oMissing,
ref oMissing, ref oMissing, ref oMissing,
ref oMissing, ref oMissing, ref oMissing,
ref oMissing, ref oMissing, ref oMissing,
ref connection, ref sqlQuery, ref oMissing,
ref oMissing, ref oMissing);
wordMerge.Destination = Word.WdMailMergeDestination.wdSendToNewDocument;
wordMerge.Execute(ref oFalse);
}
finally
{
if (wordMergeFields != null)
{
Marshal.ReleaseComObject(wordMergeFields);
}
if (wordMerge != null)
{
Marshal.ReleaseComObject(wordMerge);
}
if (doc != null)
{
Marshal.ReleaseComObject(doc);
}
}
}