I developed this a couple of years ago. It needs a bit of work however, I've just tried it out and it should give you what you want.
Another benefit to this is that it makes it easy to download .pdf attachments one after the other from Salesforce, because you can declaratively input a SOQL in the Visualforce page.
Step 1
Create a Custom Setting of type Hierarchy. Call this 'App Configurations'. Then create two fields, as per the screenshot.
Step 2
Create this Apex class.
public with sharing class AttachmentUtilityController {
//Variables
public String queryString{get; set;}
public String selectedObject{get; set;}
public String selectedField{get; set;}
public String selectedOperator{get; set;}
public String selectedValue{get; set;}
public list<SelectOption> objects{get;set;}
public list<SelectOption> returnFields{get;set;}
public list<SelectOption> returnOperators{get;set;}
public Integer maxSize{get; set;}
/**
Constructor
*/
public AttachmentUtilityController(){
objects = new list<SelectOption>();
returnOperators = new list<SelectOption>();
returnOperators.add(new SelectOption('equals', 'equals'));
returnOperators.add(new SelectOption('not equals', 'not equals'));
returnOperators.add(new SelectOption('less than', 'less than'));
returnOperators.add(new SelectOption('greater than', 'greater than'));
returnOperators.add(new SelectOption('less than or equals', 'less than or equals'));
returnOperators.add(new SelectOption('greater than or equals', 'greater than or equals'));
returnOperators.add(new SelectOption('contains', 'contains'));
returnOperators.add(new SelectOption('does not contains', 'does not contains'));
returnOperators.add(new SelectOption('starts with', 'starts with'));
returnOperators.add(new SelectOption('does starts with', 'does starts with'));
selectedObject = 'Account';
queryString = 'Select Id From '+selectedObject;
returnFields = getFields('Account');
//Get All Objects Related to attachment
Schema.DescribeFieldResult F = Attachment.ParentId.getDescribe();
List<Schema.sObjectType> P = F.getReferenceTo();
for(Schema.sObjectType sot : P){
objects.add(new SelectOption(String.Valueof(sot), String.Valueof(sot)));
}
App_Configurations__c CS = App_Configurations__c.getOrgDefaults();
maxSize = (CS.Max_File_Size__c != null ? Integer.ValueOf(CS.Max_File_Size__c) : 5120);
listAttachment = new List<Attachment>();
listAttachmentWrapper = new List<AttachmentWrapper>();
}
public static List<SelectOption> sortOptionList(List<SelectOption> ListToSort) {
if(ListToSort == null || ListToSort.size() <= 1)
return ListToSort;
List<SelectOption> Less = new List<SelectOption>();
List<SelectOption> Greater = new List<SelectOption>();
integer pivot = ListToSort.size() / 2;
// save the pivot and remove it from the list
SelectOption pivotValue = ListToSort[pivot];
ListToSort.remove(pivot);
for(SelectOption x : ListToSort)
{
if(x.getLabel() <= pivotValue.getLabel())
Less.add(x);
else if(x.getLabel() > pivotValue.getLabel()) Greater.add(x);
}
List<SelectOption> returnList = new List<SelectOption> ();
returnList.addAll(SortOptionList(Less));
returnList.add(pivotValue);
returnList.addAll(SortOptionList(Greater));
return returnList;
}
//Change Query Values
public void resetQuery(){
queryString = 'Select Id From '+selectedObject;
returnFields = getFields(selectedObject);
}
//Get fields per object
//map<string, string> mapFieldNameType;
private list<SelectOption> getFields(String obj){
List<SelectOption> options = new List<SelectOption>();
Map<String, Schema.SObjectType > gd = Schema.getGlobalDescribe();
Schema.SObjectType objType = gd.get(obj);
Schema.DescribeSObjectResult objDescribe = objType.getDescribe();
map<String, Schema.SObjectField> fieldMap = objDescribe.fields.getMap();
for(String fieldName : fieldMap.keyset()){
Schema.SObjectField field = fieldMap.get(fieldName);
Schema.DescribeFieldResult fieldDesc = field.getDescribe();
options.add(new SelectOption(fieldDesc.getName(), fieldDesc.getLabel() ));
}
options.sort();
options = sortOptionList(options);
return options;
}
//Add Condition
public void addCondition(){
if(queryString.contains('Where'))
queryString += ' AND ';
else
queryString += ' Where ';
queryString += retunQuery(selectedField, selectedOperator, selectedValue);
}
//Return Final Query
public string retunQuery(string field, string operator, string value){
string conditionString = '';
if(value != null){
if(operator == 'equals'){
conditionString = field + ' = ' + '\'' + value + '\'';
} else if( operator == 'not equals'){
conditionString = field + ' != ' + '\'' + value + '\'';
}else if( operator == 'less than'){
conditionString = field + ' < ' + value;
}else if( operator == 'greater than'){
conditionString = field + ' > ' + value;
}else if( operator == 'less than or equals'){
conditionString = field + ' <: ' + value;
}else if( operator == 'greater than or equals'){
conditionString = field + ' >: ' + value;
}
else if( operator == 'contains'){
conditionString = field + ' Like \'%' + value + '%\'';
}
else if( operator == 'does not contains'){
conditionString = '( Not ' + field + ' Like \'%' + value + '%\' )';
}
else if( operator == 'starts with'){
conditionString = field + ' Like \'' + value + '%\'';
}
else if( operator == 'does starts with'){
conditionString = '( Not ' + field + ' Like \'' + value + + '%\' )';
}
}
return conditionString;
}
//Method to fetch all attachment
public List<AttachmentWrapper> listAttachmentWrapper{get;set;}
public List<Attachment> listAttachment{get;set;}
public PageReference fetchAttachments(){
try {
listAttachmentWrapper = new List<AttachmentWrapper>();
set<string> setParentIds = new set<string>();
try {
for(SObject sObj : Database.query(queryString)){
setParentIds.add(sObj.Id);
}
}
catch(Exception ex) {
ApexPages.addMessage(new ApexPages.message(ApexPages.severity.ERROR,'Invalid Query : ' + ex.getmessage()));
}
listAttachment = [Select Name, Id, ContentType, ParentId, BodyLength From Attachment Where ParentId IN : setParentIds];
for(Attachment attach : listAttachment){
listAttachmentWrapper.add(new AttachmentWrapper(false, attach));
}
}
catch(Exception ex) {
ApexPages.addMessages(ex);
}
return ApexPages.currentPage();
}
//Method to send Page
public List<AttachmentWrapper> selectedAttachmentWrapper{get;set;}
public PageReference next(){
string attachIds = '';
for (AttachmentWrapper a : listAttachmentWrapper){
if(a.isSelected){
if(attachIds == '')
attachIds = a.attach.Id;
else
attachIds += ','+ a.attach.Id;
}
}
Pagereference pg = new Pagereference('/apex/SendEmail?attachIds='+attachIds);
pg.setRedirect(true);
return pg;
}
//Wrapper Class
public class AttachmentWrapper{
public boolean isSelected{get; set;}
public Attachment attach{get; set;}
public Decimal bodyLengthInMB{get; set;}
public AttachmentWrapper(boolean isSelected, Attachment attach){
this.isSelected = isSelected;
this.attach = attach;
this.bodyLengthInMB = ( attach.BodyLength ) / ( 1024 * 1024 * 1.00 );
//this.bodylengthInMB = bodylengthInMB.setScale( 2, RoundingMode.CEILING);
}
}
}
Step 3
Create this Test class.
@isTest(seeAllData = false)
public class AttachmentUtilityControllerTest {
//Unit Test Method
static testMethod void unitTest01() {
String folderId = [Select Id From Folder limit 1].id;
//Create Custom setting
App_Configurations__c CS = new App_Configurations__c();
CS.Folder_Id__c = folderId;
insert CS;
//Create Account
Account account = new Account(Name = 'Test');
insert account;
//Create Attachment
Attachment a = new Attachment();
a.name= 'Test Attachment Name';
a.body= blob.valueOf('Test Body');
a.parentID= account.id;
insert a;
//Start Test method
Test.startTest();
AttachmentUtilityController adc = new AttachmentUtilityController();
adc.resetQuery();
adc.fetchAttachments();
adc.saveToDocument();
Test.stopTest();
//End of test method
}
}
Step 4
Create this Visualforce page. Call the page 'AttachmentUtility'.
<apex:page controller="AttachmentUtilityController" id="pgId" sidebar="false">
<apex:pageMessages id="pgMsg"/>
<apex:sectionHeader subtitle="Attachment Downloader" title="Download Attachments"/>
<script>
function callNext(){
if( totalFileSize <= 0 ){
alert('Please select atleast one attachment');
return false;
}
else if( totalFileSize > totalFileSizeAllowed ){
alert('Selected item size exceeds the limit : ' + '{!maxSize}' + ' bytes.' );
return false;
}
else {
jsNext();
return true;
}
}
var totalFileSize = 0;
// take this from custom setting
var totalFileSizeAllowed = {!maxSize};
function calculateFileSize( chkCtrl, fileSize ) {
try {
var selectAllid = document.getElementById("pgId:fromId:pbId:opResultantPBSId:pbtId:slctAll");
if(document.getElementById("pgId:fromId:pbId:opResultantPBSId:pbtId:slctAll").checked) {
document.getElementById("pgId:fromId:pbId:opResultantPBSId:pbtId:slctAll").checked = chkCtrl.checked;
} else {
var isAllselect = true;
for(var i=0; i<{!listAttachmentWrapper.size}; i++) {
var id = "pgId:fromId:pbId:opResultantPBSId:pbtId:"+i+":slctSingle";
if(!document.getElementById(id).checked ) {
isAllselect = false ;
break ;
}
}
if(isAllselect) {
document.getElementById("pgId:fromId:pbId:opResultantPBSId:pbtId:slctAll").checked = true;
}
}
if( chkCtrl.checked ) {
totalFileSize = totalFileSize * 1 + fileSize * 1;
}
else {
totalFileSize = totalFileSize * 1 - fileSize * 1;
}
if( totalFileSize > totalFileSizeAllowed ) {
// make this "opLimit" output panel visisble
document.getElementById("pgId:fromId:pbId:opResultantPBSId:olLimit").style.visibility = 'visible';
}
else {
// make this "opLimit" output panel hidden
document.getElementById("pgId:fromId:pbId:opResultantPBSId:olLimit").style.visibility = 'hidden';
}
document.getElementById('pgId:fromId:pbId:opResultantPBSId:opFileSizeArea').innerHTML = 'Combined file size of selected attachements: <B>'+totalFileSize+'</B>';
}
catch(e) {
alert(e);
}
return false;
}
function checkAll(cb){
totalFileSize = 0;
for(var i=0; i<{!listAttachmentWrapper.size}; i++) {
document.getElementById("pgId:fromId:pbId:opResultantPBSId:pbtId:"+i+":slctSingle").checked = cb.checked;
if(cb.checked){
var fileSize = document.getElementById("pgId:fromId:pbId:opResultantPBSId:pbtId:"+i+":singleSelectHidden").value
totalFileSize = totalFileSize*1 + fileSize*1;
}
}
if( totalFileSize > totalFileSizeAllowed ) {
// make this "opLimit" output panel visisble
document.getElementById("pgId:fromId:pbId:opResultantPBSId:olLimit").style.visibility = 'visible';
}
else {
// make this "opLimit" output panel hidden
document.getElementById("pgId:fromId:pbId:opResultantPBSId:olLimit").style.visibility = 'hidden';
}
document.getElementById('pgId:fromId:pbId:opResultantPBSId:opFileSizeArea').innerHTML = 'Combined file size of selected attachments: <B>'+totalFileSize+'</B>';
}
</script>
<!-- Form -->
<apex:form id="fromId">
<apex:actionFunction name="jsResetObject" action="{!resetQuery}" reRender="opTextArea, opField"/>
<apex:actionFunction name="jsNext" action="{!next}" reRender="pgMsg"/>
<apex:actionFunction name="jsAddCondition" action="{!addCondition}" reRender="pgMsg, opTextArea"/>
<!-- Block for Search Attachment -->
<apex:pageBlock id="pbId" title="Search Attachments" >
<apex:pageBlockButtons id="pbbId" location="top">
<apex:commandButton value="Get Attachment" action="{!fetchAttachments}"/>
</apex:pageBlockButtons>
<apex:pageBlockSection id="pbsId" columns="1">
<!-- Section Item for Object Picklist -->
<apex:pageBlockSectionItem >
<apex:outputLabel value="Object"/>
<apex:outputPanel >
<apex:selectList size="1" label="Object" value="{!selectedObject}" onchange="jsResetObject();return false;">
<apex:selectOptions value="{!objects}"/>
</apex:selectList>
</apex:outputPanel>
</apex:pageBlockSectionItem>
<!-- Section Item for Object Picklist -->
<apex:pageBlockSectionItem >
<apex:outputLabel value="Where Clause"/>
<apex:outputPanel id="opField" >
<apex:selectList size="1" label="Field" value="{!selectedField}">
<apex:selectOptions value="{!returnFields}"/>
</apex:selectList>
<apex:selectList size="1" label="Operator" value="{!selectedOperator}">
<apex:selectOptions value="{!returnOperators}"/>
</apex:selectList>
<apex:inputText id="valueId" value="{!selectedValue}"/>
<apex:commandButton value="Add Condition" onclick="jsAddCondition();return false;"/>
</apex:outputPanel>
</apex:pageBlockSectionItem>
<!-- Section Item for Search Query -->
<apex:pageBlockSectionItem >
<apex:outputLabel value="Search Query"/>
<apex:outputPanel id="opTextArea" >
<apex:inputTextarea value="{!queryString}" style="margin-left:0px;width:500px;" rows="5"/>
</apex:outputPanel>
</apex:pageBlockSectionItem>
</apex:pageBlockSection>
<!-- Block for Resultant Attachment -->
<apex:outputPanel id="opResultantId">
<apex:pageBlockSection title="Attachments" columns="1" id="opResultantPBSId">
<apex:commandButton value="Email Attachments" style="margin-left: 32%;" onclick="callNext(); return false;" rendered="{!listAttachmentWrapper.size > 0}" />
<apex:pageMessage severity="info" strength="3" summary="No record found" rendered="{!listAttachment.size = 0}"/>
<apex:outputLabel rendered="{!listAttachmentWrapper.size > 0}"> Total Number of Records : <B> {!TEXT(listAttachmentWrapper.size)} </B> </apex:outputLabel>
<apex:outputLabel rendered="{!listAttachmentWrapper.size > 0}" id="opFileSizeArea"></apex:outputLabel>
<apex:outputLabel id="olLimit" style="color: red; font-weight: bold;" > File size Exceeds the limit : {!maxSize} bytes.</apex:outputLabel>
<script>
document.getElementById("pgId:fromId:pbId:opResultantPBSId:olLimit").style.visibility = 'hidden';
</script>
<apex:pageBlockTable id="pbtId" value="{!listAttachmentWrapper}" var="la" rendered="{!listAttachmentWrapper.size > 0}">
<apex:column >
<apex:facet name="header">
<apex:inputCheckBox id="slctAll" onclick="checkAll(this);" />
</apex:facet>
<apex:inputCheckbox id="slctSingle" value="{!la.isSelected}" onchange="calculateFileSize(this, '{!la.attach.BodyLength}');"/>
<apex:inputhidden value="{!la.attach.BodyLength}" id="singleSelectHidden"/>
</apex:column>
<apex:column headerValue="Name">
<apex:outputLink value="/{!la.attach.Id}" target="_blank">{!la.attach.Name}</apex:outputLink>
</apex:column>
<apex:column headerValue="File Size (Bytes)">
<apex:outPutText value="{0, number, ###,###,###,##0}">
<apex:param value="{!la.attach.BodyLength}"/>
</apex:outPutText>
</apex:column>
<apex:column headerValue="File Size (MB)">
<apex:outPutText value="{0, number, ###,###,###,##0.00000}">
<apex:param value="{!la.bodyLengthInMB}"/>
</apex:outPutText>
</apex:column>
<apex:column >
<apex:outputLink value="{!URLFOR($Action.Attachment.Download, la.attach.id)}" target="_blank">
Download to Desktop
</apex:outputLink>
</apex:column>
</apex:pageBlockTable>
</apex:pageBlockSection>
</apex:outputPanel>
</apex:pageBlock>
</apex:form>
</apex:page>
Step 5
Go to Setup | Develop | Visualforce Pages and click on the page that you have just created.
You will see that you can query all of the attachments per objects in your Org and you can input a SOQL declaratively to return the set that you are interested in. You can use all of the operators that you would expect from the Salesforce report writer and Workbench.
Step 6
In this example, I've used a SOQL to return the set of Opportunities for whose attachments I wish to email. Simply select the attachments you wish to email on the right hand side, then select 'Email Attachments'.
Step 7
You will then be auto-navigated to this screen here, where you will have to input values into the mandatory fields else a validation will be hit. When you're ready, select 'Send'.
In addition to this, there are a number of 'email attachment' apps on the AppExchange.
Hope this helps.