Force.com REST とStreaming API 〜 Winter '12リリース 〜
by Mitsuhiro Okamoto on 11月 4, 2011 at 10:03 午後
Spring '11で正式リリースされたREST APIと、現在パイロットリリース中のStreaming APIですが、Winter '12ではいくつかの機能追加及び変更がありました。以下詳細です。
REST APIでバイナリデータサポート改善
REST APIでのバイナリデータ(Blob)アップロードは従来50MBまででしたが(base64エンコード後のサイズ)、Winter '12リリースでは、multipartタイプを使用して500MBまでのblobファイルをアップロード出来るようになりました。
multipart/form-dataで送信されるデータの構造はEmailのraw dataと似た構成になっています。
--boundary_string
Content-Disposition: form-data; name="entity_document";
Content-Type: application/json
{
"Description" : "Marketing brochure for Q1 2011",
"Keywords" : "marketing,sales,update",
"FolderId" : "005D0000001GiU7",
"Name" : "Marketing Brochure Q1",
"Type" : "pdf"
}
--boundary_string
Content-Type: application/pdf
Content-Disposition: form-data; name="Body";
filename="2011Q1MktgBrochure.pdf"
ここにバイナリデータが入る
--boundary_string--
より詳しい情報は最新のREST APIドキュメント(英語)を御覧ください。
Streaming APIのOAuth tokenの扱い変更
Streaming APIのSummer '11リリースでは OAuth Tokenのやりとりにcookieを利用していましたが、Winter '12リリースではHTTP Authorizationヘッダに付与するように変更されました。
Authorization: OAuth sessionId
Streaming APIのcannelのprefixに"/topic/"が付与
Streaming APIのWinter '12でのもうひとつの大きな変更として、Bayeux(CometD)プロトコルで送信されるデータのcannel名には、"/topic/" + PushTopic名が用いられるようになりました。これによって、例えばjQueryのCometDバインドを使った例では以下の様に記述するようになります。
var cometdUrl = 'https://' + window.location.hostname + '/cometd/23.0/';
var auth = 'OAuth {!$Api.Session_ID}';
$.cometd.init({
url: cometdUrl,
requestHeaders: { Authorization: auth }
});
$.cometd.subscribe('/topic/AccountTopic', function(message) {
//ここに受信時の処理を記述
});
Streaming APIにおけるタイムアウトの変更
最後のWinter '12における大きな変更は、セッションタイムアウト値がhandshakes、connects、subscribesのたびにリセットされるようになりました。詳細は最新のSreaming APIドキュメント(英語)に記載されています。
Streaming APIはWinter '12の時点ではパイロットリリースとなっています。実際にDeveloper Editionで試してみたい方はセールスフォース・ドットコムへお問合せ下さい。
Streaming APIはまだ開発途中の機能ですが、リアルタイムなPush通知が実装できるようになるなど、非常に大きな可能性を秘めています。Database.comでも使えるようになる予定ですので、是非ご期待下さい。
Force.com FlowをVisualforceと合わせて使う
by Mitsuhiro Okamoto on 10月 31, 2011 at 07:26 午後
Winter '12リリースでついにクラウドベースのFlow DesignerがBetaリリースされました。Flow Designerを使えば、今まではVisualforceを利用しなければ作成出来なかった、複数画面に渡るウィザードや入力内容によって条件分岐する画面などがコードを書かなくとも簡単に作成・編集できるようになります。
またApexクラスからやVisualforceタグを使ってFlowのコンポーネントへアクセスが可能ですので、Visualforce内にFlowページを埋め込むといったことも可能です。以下はFlowで入力されたCase番号からVFページ上にデータを読み込むサンプルです。
VisualforcePage : caseflow
<apex:page controller="FlowpageController" sidebar="false">
<div style="float: left; width: 400px; padding: 10px;">
<flow:interview name="CaseFlow" interview="{!caseFlow}" reRender="myPanel" />
</div>
<apex:outputPanel id="myPanel" style="float: left; min-width:400px; padding: 10px;">
<apex:pageBlock title="ケース内容">
<apex:pageBlockSection columns="1">
<apex:outputField value="{!sourceCase.ID}"/>
<apex:outputField value="{!sourceCase.CaseNumber}"/>
<apex:outputField value="{!sourceCase.Subject}"/>
</apex:pageBlockSection>
</apex:pageblock>
</apex:outputPanel>
</apex:page>
<flow:interview>タグによってflowをVFページ上に配置できます。このタグはreRender属性を持っているので、フローの遷移時にダイナミックに再描画する箇所を指定できます。
以下はApexコントローラクラスです。
Apex Class : FlowpageController
public with sharing class FlowpageController {
public Flow.Interview.CaseFlow caseFlow {get;set;}
public Case sourceCase{
get{
try{
List<Case> results = [SELECT Id,CaseNumber,Subject FROM Case Where CaseNumber = :caseFlow.vaCaseNo];
if(results.size() !=0){
return results[0];
}
}catch(System.QueryException e){
//just Demo, ignore;
}
return new Case();
}
set;
}
public FlowpageController(){
caseFlow = new Flow.Interview.CaseFlow(new Map<String, Object>());
}
}
こちらではFlow.Interview.<フロー名>というクラスが宣言されている所が確認できます。
エンドユーザの方はForce.com Flowだけを使って画面開発することが多いでしょうが、開発者の方は是非Visualforceともインテグレーションして、より高度な画面デザインにチャレンジしてみて下さい。
セキュアコーディングガイドライン日本語版公開
by Mitsuhiro Okamoto on 9月 13, 2011 at 02:29 午後
DeveloperForceグローバルに公開されているコンテンツ「Secure Coding Guideline」の日本語版を公開しました。
セキュアコーディングガイドラインはForce.comでエンタープライズ用途に耐えうるセキュアなアプリケーションを作成する上で必須の知識がまとめられています。
XSS(クロスサイトスクリプティング)やCSRF(クロスサイトリクエストフォージェリ)等のスクリプトによる脆弱性から、SOQL/SOSLインジェクション、Cookies盗用、リダイレクト、SSOセキュリティ等の多岐に渡るトピックを、Apex/Visualforceでのケース以外にもJava、.Net、PHP、Rubyなどから扱う場合にも包括的に解説しています。
AppExchange/OEMアプリケーションのセキュリティレビューにおいても、本ガイドラインへの順守がレビューの対象となっています。
カスタムアプリケーションを開発するSIer、連携アプリケーションを開発するパートナー、企業のセキュリティ担当者など、Force.comに関わる全ての方にお薦め致します。ぜひご一読下さい。
セキュアコーディングガイドライン
http://wiki.developerforce.com/index.php/JP:Secure_Coding_Guideline
Apexテストコード作成の落とし穴
by Tetsuo Ajima on 7月 4, 2011 at 01:45 午後
本エントリでは、Apexテストコードを作成する際に注意しなければならない「落とし穴」をいくつかご紹介したいと思います。
この記事の対象者
- Apexのテストを「なんとなく」書いている人
- Apexのテストが動かなくなって失敗した経験のある人
- Apexのテストのコードレビューのポイントを知りたい人
Apexテストコードを実装するための基本的な知識は下記を参照してください。
ガバナ制限
落とし穴:
本番の運用が進みデータが増大したため、作成したテストが半年前の運用開始時には実行できていたが、現在は失敗するようになってしまった
注意点:
データが増大した場合でもガバナ制限に抵触せず、実行可能なコードを書くこと
解説:
開発時と本番環境、初回リリース時と運用開始から数ヶ月後など、データ量の違いによりテストが動作したりしなくなったりする場合があります。
特に多く見られるのが、SOQLで取得できる件数やDML処理できる件数がガバナ制限を超えるケースです。
このようなことを防ぐために、常に最大データ量を意識しながらテストクラスを実装する必要があります。
テスト前にテーブルを空にしたいという意図から、テスト前の準備処理で全件検索・全件削除を行う実装が時折見られますが、これも当然NGです。テスト前の準備処理では、データの削除・作成は必要最低限であるべきです。
@IsTest
private class MyTestClass {
private static testMethod void myTest() {
// データの準備
delete [Select Id From CustomObject__c]; // NG!
CustomObject__c c = new CustomObject__c(Name='hoge');
insert c;
Test.startTest();
String result = target.methodA();
Test.stopTest();
System.assertEquals('sample', result);
}
}また、1つのテストメソッド中で複数のテストを行ってしまうのもNGパターンです。 Apexのテストは、テストメソッド1つが1トランザクションとしてガバナ制限が計算されます。そのため、テスト対象の処理がそれのみでガバナ制限ぎりぎりで設計されている場合は複数のテストをまとめた結果、トータルで制限を超えてしまう場合もあります。 1つのテストメソッドは1つのテストしかしないように実装しましょう。テスト対象のメソッドがそれのみでガバナ制限ぎりぎりの場合は、Test.startTest()とTest.stopTest()を活用してテストの準備処理をテスト対象の処理のガバナ制限を切り離すようにしましょう。
Test.startTest() / Test.stopTest()
落とし穴:
Test.startTest() / Test.stopTest()を使ったのにガバナ制限が合計で計算されてしまった
注意点:
Test.startTest()が呼ばれてから最初のDMLまたはWeb Serviceメソッド呼び出してガバナ制限がリセットされる
解説:
Test.startTest() / Test.stopTest()を呼び出すと、その前後の処理とはガバナ制限が別計算されます。そのため、テスト対象の処理がそれのみでガバナ制限ぎりぎりの場合に有効な手段です。そうでないばあいでも習慣的に利用するようにしておくと良いでしょう。
しかし、Test.startTest()内でDMLまたはWeb Serviceメソッドが呼び出されるとそのタイミングでガバナ制限の計算がリセットされるという仕様を理解しておく必要があります。テスト対象の処理が検索のみの場合は、テストとは直接関係ないDML処理を書いておく等のテクニックも必要になる場合があります。
@IsTest
private class MyTestClass {
private static testMethod void myTest() {
// データの準備
List exsists = [Select Id From CustomObject__c Where Name='hoge'];
if (!exsists.isEmpty()) {
delete exsists;
}
CustomObject__c c = new CustomObject__c(Name='hoge');
insert c; // テスト用データの作成
Test.startTest();
Account a = new Account(Name='hoge');
insert a; // テストとは直接関係ないが、ガバナ制限をリセットさせるためのDML呼び出し
String result = target.methodA(); // テスト対象の処理にはDMLが無い
Test.stopTest();
System.assertEquals('sample', result);
}
}
データ依存
落とし穴:
あるデータが存在することが前提となっているコードになっているため、開発環境で成功していたテストが本番移行時には失敗してしまった
注意点:
データが存在することも存在しないことも前提にした実装をしてはいけない
解説:
テストメソッド中で使いたいデータは、まずSOQLを実行してデータの存在有無を必ずチェックしましょう。そしてデータが既にあれば削除して作成し(またはそのまま使う)、データがなかったら新規に作成するというのが基本的な戦略です。
開発環境では既にテストデータが入っていることも多いため、データが存在することが前提になっているコードを書いてしまう場合があるので注意が必要です。
@IsTest
private class MyTestClass {
private static testMethod void myTest() {
// データの準備
List exsists = [Select Id From CustomObject__c Where Name='hoge']; // まず検索
if (!exsists.isEmpty()) {
delete exsists; // 既存データがあれば削除
}
CustomObject__c c = new CustomObject__c(Name='hoge');
insert c; // テスト用データの作成
Test.startTest();
String result = target.methodA();
Test.stopTest();
System.assertEquals('sample', result);
}
} 逆に、あるデータが存在しないことが暗黙的に前提となってしまっている場合にも注意が必要です。たとえば、テスト中でデータを作成したら組織内の既存データと重複したデータができてしまったり、一意性制約違反でエラーとなったりするケースが挙げられます。
どちらの場合も環境間でデータの状況が異なると気づきにくいポイントです。
ユーザ名重複
落とし穴:
テスト中でrunAs()に渡すために'test@example.com'というユーザを作成しているが、Sandboxではテストが成功したのに本番環境ではテストが失敗した
注意点:
ユーザ名は、世界中の全組織間でユニークである必要がある
解説:
System.runAs(User){...}を使うと、そのブロック内は指定したユーザとして実行できます。そのため、権限別のテストや可視範囲別のテストを行うのに適しています。
しかしSalesforceのユーザ名は、ログイン画面が共通であることからもわかるように、全組織間でユニークである必要があります。ただし、本番/Trial/DE組織 と Sandboxでは別管理のため、Sandboxで作成できたユーザが本番では作成できない場合やその逆の場合もあります。
他組織で作成されたユーザと名前が重複しないためには次の点に留意します。
- 誰でも思いつくような、サンプルによく使われるようなユーザ名をつけないこと
- @以降は自社ドメインにしておく。確実な対処方法ではないが、他組織と重複する可能性は低くなる
- ユーザ名をハードコーディングせずカスタム設定で定義し、適宜変更できるようにしておく
@IsTest
private class MyTestClass {
private static testMethod void myTest() {
// データの準備
List exsists = [Select Id From CustomObject__c Where Name='hoge'];
if (!exsists.isEmpty()) {
delete exsists;
}
CustomObject__c c = new CustomObject__c(Name='hoge');
insert c; // テスト用データの作成
User u = new User(); // テスト用ユーザの作成
u.Name = 'test@example.com'; // NG! よく使われそうなユーザ名
u.Email = 'test@example.com';
u.LastName = 'LastName';
u.profileid = UserInfo.getProfileId();
u.emailencodingkey='ISO-8859-1';
u.languagelocalekey='en_US';
u.localesidkey='en_GB';
u.timezonesidkey='Europe/London';
String result;
Test.startTest();
System.runAs(u) {
result = target.methodA();
}
Test.stopTest();
System.assertEquals('sample', result);
}
}
トリガのカバー
落とし穴:
開発時に組織全体でテストカバー率75%以上を満たしていることを確認しているにも関わらず、一度も実行されなかったトリガがあったため本番移行時にデプロイが失敗した
注意点:
各Apexトリガのテストカバー率は0%ではいけない
解説:
デプロイの際のApexテストでは、各Apexトリガは1度は実行されなければいけないというルールがあります。
全テストを実行したときに、組織全体のテストカバー率が75%を超えていることだけでなくテストカバー率が0%のトリガが無いことを確認しましょう。
ハードコーディング
落とし穴:
絶対URLやレコードIdをハードコーディングしていたため、開発環境で動作するテストを本番環境へ移行できなかった
注意点:
ホスト名やレコードIdは組織ごとに異なる場合がある
解説:
URLが必要な場合、通常は絶対URLではなく相対URLを利用しましょう。どうしても絶対URLが必要な場合は、絶対URLやホスト名はSystem.URLクラスから取得できます。
レコードIdはクエリで取得しましょう。以前は、画面上にレポートへのリンクを実装したい場合等に、レポートのIdはSOQLクエリで取得できなかったためにId値をハードコーディングしてしまう例がよく見られました。しかしAPIバージョン20.0よりreportオブジェクトから名前をキーにしたクエリが可能になりました。
どうしてもId値を動的に取得できない場合はカスタム設定の活用を推奨します。
@IsTest
private class MyTestClass {
private static testMethod void myTest() {
// データの準備
List exsists = [Select Id From CustomObject__c Where ID='a0S000000000004XVC']; // NG!
if (!exsists.isEmpty()) {
delete exsists; // 既存データがあれば削除
}
CustomObject__c c = new CustomObject__c(Name='hoge');
insert c; // テスト用データの作成
Test.startTest();
String result = target.methodA();
Test.stopTest();
System.assertEquals('sample', result);
}
}
自動採番項目の欠番
落とし穴:
自動採番項目が0から順番に振られることを期待していたが、本番運用時に開始番号が違ったり欠番が出たりした
注意点:
自動採番はテスト実行後もロールバックされない
解説:
自動採番項目の値は、一般的なRDBMSのシーケンスオブジェクトと同様の考え方により、パフォーマンスとスレッドセーフを両立させるためトランザクション外で採番されます。そのため、トランザクションがロールバックされても採番値はインクリメントされたまま元に戻りません。
自動採番項目に振られる値を想定した実装・運用をしてはいけません。
Mixed DML Operation
落とし穴:
テストの中でキューを作成したら実行できなかった
注意点:
いくつかの設定系オブジェクトとトランザクションデータ系オブジェクトは同一トランザクション内でのDML実行に制限がある
解説:
下記のオブジェクトは、トランザクション系データと同一のトランザクションでのDML処理に制限があります。
Group / GroupMember
QueueSObject
User / UserRole
UserTerritory / Territory
Custom settings
デプロイ前にこれらを作成する手順を考える等の工夫が必要になります。
詳細は下記を参照してください。
“sObjects That Cannot Be Used Together in DML Operations”
Apexコード量
落とし穴:
開発したApexコード量が増えるにつれ、テストコードも増大して組織のApexコード使用量(2MB)のガバナ制限を超えてしまった
注意点:
テストはProductionコードとは別クラスに定義し、@IsTestアノテーションを付与する
解説:
組織のApexコード使用量は2MBまで(正確には、コメントを除いて200万文字。スペース、タブ、改行を含む)というガバナ制限があります。ただし、これには@IsTestアノテーションが付与されたクラスはカウントされません。
@IsTestアノテーションが付与できるのはprivateクラスのみです。そのため、複数クラスで共通に利用するロジックは記述できないことに注意してください。
public class MyTestClass { // NG!
private static testMethod void myTest() {
// データの準備
List exsists = [Select Id From CustomObject__c Where Name='hoge']; // まず検索
if (!exsists.isEmpty()) {
delete exsists; // 既存データがあれば削除
}
CustomObject__c c = new CustomObject__c(Name='hoge');
insert c; // テスト用データの作成
Test.startTest();
String result = target.methodA();
Test.stopTest();
System.assertEquals('sample', result);
}
} ちなみに、WebUI上でソースコードの編集を行うとタブはすべてスペース4つに変換されます。WebUI上での編集によってコードの文字数が増大する場合もあることを念頭に入れておきましょう。
まとめ
Apexのテストは移植性(ポータビリティ)が非常に重要です。
組織上のデータの状態にかかわらず、半永久的に実行できるコードを書かなければいけません。
また、本エントリでご紹介した落とし穴のいくつかはテストコードに限らず通常のProductionコードにもそのまま当てはめることのできるTipsです。
紹介した落とし穴にみなさまがはまらないよう、本エントリがお役にたてれば幸いです。
ガバナ制限の一覧(バージョン22.0 - Summer'11)
by Tetsuo Ajima on 6月 2, 2011 at 05:02 午後
ガバナ制限の一覧(バージョン22.0 - Summer'11)の日本語訳を作成しました。開発者のみなさま、どうぞご利用ください。
正式リリース版対応です。
ガバナ制限の一覧(バージョン22.0 - Summer'11)
今回は、@futureメソッドやBatch Apexなど、非同期処理の制限緩和が目立ちますね。
※おねがい
リンクしていただく場合はファイルへの直リンクではなく、このブログ記事へのリンクとしていただきますよう、お願い申し上げます。修正が入った際にファイルのURLが変わる場合がございます。
2011/06/29 追記: オリジナルが更新されていたので、そちらに合わせてこちらも更新しました。
3月30日の1カ月前は3月1日? - Datetime型利用時の注意
by Tetsuo Ajima on 4月 4, 2011 at 05:29 午後
3月30日の1カ月前は3月1日?
下記のApexコードを実行すると、デバッグログにはどのように出力されるでしょうか?
Datetime dt = Datetime.newInstance(2011, 03, 30); // (1)
dt = dt.addMonths(-1); // (2)
String result = dt.format('yyyy/MM/dd'); // (3)
System.debug(result);
このコードでは、2011年3月30日の1カ月前の日付を"yyyy/MM/dd"形式で求めようとしています。
しかし、実行結果は"2011/03/01" になります。
どうしてこのようなことが起こるのか?
上記のコードは、次のようにして動作しています。
(1)
実行ユーザーのローカル時刻で2011年3月30日0時0分0秒を指すDatetime型のインスタンスが作成される
(2)
1カ月前の日時が計算される
(3)
実行ユーザーのローカル時刻でフォーマットされた文字列が返る
注意しなければいけない点は、上記(2)の処理です。(2)の計算の結果、存在しない値(2月30日など)となった場合、存在しうる値(うるう年でなければ2月28日など)に丸められますが、ローカルタイムゾーンは考慮されず、GMTベースで丸め処理が行われます。
処理の流れをもう少し詳細に記述すると下記のようになります。
(1)
実行ユーザーのローカル時刻(日本時間)で2011年3月30日0時0分0秒 (GMTでは2011年3月29日15時0分0秒) を指すDatetime型のインスタンスが作成される。
(2)
1カ月前の日時が計算される。2011年3月29日15時0分0秒(GMT)の1カ月前は2011年2月29日15時0分0秒(GMT)となり、うるう年でなければ2011年2月28日15時0分0秒(GMT)に丸められる。
(3)
実行ユーザーのローカル時刻(日本時間)でフォーマットされた文字列が返る。2011年2月28日15時0分0秒(GMT)を日本時間に換算すると、2011年3月1日0時0分0秒となり、"2011/03/01"が返る。
GMT以外のタイムゾーンを利用するすべてのユーザーが同じ影響を受けます。
日本時間では、addMonths()の結果が下記の日時になる場合に注意が必要です。
02/29 00:00:00 - 08:59:59 (うるう年を除く)
02/30 00:00:00 - 08:59:59
02/31 00:00:00 - 08:59:59
04/31 00:00:00 - 08:59:59
06/31 00:00:00 - 08:59:59
09/31 00:00:00 - 08:59:59
11/31 00:00:00 - 08:59:59
また、addYears()メソッドにも類似の注意点が存在します。
回避策
「指定したある日付の1カ月前を求める」という要件であれば、すべての値をGMTで揃えるのが最も安全な方法です。
Datetime dt = Datetime.newInstanceGmt(2011, 03, 30); // (1)
dt = dt.addMonths(-1); // (2)
String result = dt.formatGmt('yyyy/MM/dd'); // (3)
System.debug(result);
このコードでは、2011年3月30日(GMT)の1カ月前の日付を"yyyy/MM/dd"形式で求めています。
Datetime#newInstance()の代わりにDatetime#newInstanceGmt()を使うと、引数に指定した値をGMTとして解釈してDatetime型のインスタンスが作成されます。
また、Datetime#format()の代わりにDatetime#formatGmt()を使うと、GMT換算でフォーマットされた文字列が返ります。
別のアプローチとして、Datetime型ではなくDate型を使う方法もあります。Date型にはタイムゾーンという概念が無いため、気にせずにaddMonths()を使うことができます。
ただし、Date型ではformat()に書式を指定できないため、year()、month()、daty()の各メソッドを使います。
Date d = Date.newInstance(2011, 3, 30);
d = d.addMonths(-1);
String result = d.year() + '/' + d.month() + '/' + d.day();
System.debug(result);
Apex開発者ガイド日本語版(Beta)公開
by Mitsuhiro Okamoto on 5月 10, 2010 at 10:42 午前
現在Apex Developer Guide(Apex開発者ガイド)の日本語版作成を進めておりますが、いち早く参考にしたいとのご要望を多く頂きましたので、取り急ぎBeta版として公開致しました。現在最新のSpring 10'(Version 18)のドキュメントをベースにしております。
一部サンプルコード内の改行等に不具合がございますが、下記よりダウンロード頂けますので是非ご参考下さい!!
アプリケーションロジック
http://wiki.developerforce.com/index.php/JP:App_Logic
Apex APIで電子メールを送信
by Shinichi Tomita on 5月 28, 2007 at 06:37 午後
Spring'07からApex APIのバージョンは9.0になりましたが、9.0で追加されたAPIの新機能に「電子メール送信(sendEmail)」というものがあります。これによって、アプリケーションは電子メール送信サーバ(SMTPサーバ)を介することなく、Salesforceから直接指定した宛先に対してメールを送信することが可能になります。
この新機能はAJAX Toolkitを使ってSコントロールから利用することもできます。以下にAJAX Toolkitからの利用例を示します。
var message = new sforce.SingleEmailMessage();
message.toAddresses = [ 'abc@example.com', 'def@example.org']; // 送信先メールアドレス(複数可)
message.subject = 'Hello, World';
message.plainTextBody = 'Hello World from '+sforce.connection.getUserInfo().userFullName;
var result = sforce.connection.sendEmail([ message ]);
Salesforceのデータベースにメールの宛先に指定したいレコード情報(e.g. リード、取引先責任者など)が格納されている場合は、IDによって宛先を指定することも可能です。
var message = new sforce.SingleEmailMessage();
message.targetObjectId = '00Q5000000HHUaw'; // リードのID
message.subject = 'Hello, World';
message.plainTextBody = 'Hello World from '+sforce.connection.getUserInfo().userFullName;
var result = sforce.connection.sendEmail([ message ]);
SingleEmailMessageのほかにMassEmailMessageを利用することで、Salesforceに保存されている電子メールテンプレートを利用して、一括メール送信を行うことも可能です。
var message = new sforce.MassEmailMessage();
message.targetObjectIds = [ '00Q5000000HHUaw', '00Q5000000HHUYQ' ]; // メールを一括送信するリードをIDの配列で指定
message.templateId = '00X50000000o1Gk'; // 使用する電子メールテンプレートのID
var result = sforce.connection.sendEmail([ message ]);
なお、メール送信者の名前およびメールアドレスには、APIにログインしているユーザの情報が用いられることに注意してください。
こちらのAPIについての詳細はApex Web Services API Developers Guideに記載されています。
また、ADN英語版のApex WikiにはSコントロールで電子メール送信を実装したサンプルがあがっていますので、ぜひ参考にしてみてください。
http://wiki.developerforce.com/index.php/Send_Emails_Through_SControls

