There are multiple issues related to this solution and in fact SAP has released a knowledge article to the topic that it is not allowed citing security reasons - SAP KBA: 1622881 - Approve by E-mail and Reject by E-mail functionality but there are certainly workarounds available.
The security issues, mainly, are:
• Validating correct approver and delegate approvers
• Emails could be sent with From option in mails making it even more difficult to validate
However, I did try to implement the process and succeeded in doing so with few (not recommended) workarounds.
My main motivation came from this link where a similar solution is suggested but for SAP Workflow:
http://www.****************/Tutorials/Workflow/offline/Index.htm
The BASIS configurations remain the same as given in the above link: The steps are as follows:
1) Create Offline User in SAP (It could be a new user if the approver will forward the mail to approve or reject requests, in case of reply back it has to be WF-BATCH)
2) Configure the SAP-Connect node via SICF Transaction
3) Configure and activate the SMTP Service via SMICM transaction
4) Configure and set the Inbound E-Mail Exit Configuration
Even the next few steps remain the same, only the actual approval process has to be changed. In the 4th step, we need to provide a class name to process emails. In this example, I named the class as: Z_PROCESS_INBOUND_WORKFLOW. Add Interface to the class: IF_INBOUND_EXIT_BCS. You will see 2 methods added from the interface.
Add the code in the methods:
Z_PROCESS_INBOUND_WORKFLOW->IF_INBOUND_EXIT_BCS~CREATE_INSTANCE
Here, we need to create an instance of the class to be used for further processing.
Sample Code below:
DATA: lo_ref TYPE REF TO z_process_inbound_workflow.
* check if the instance is initial
IF lo_ref IS INITIAL.
CREATE OBJECT lo_ref.
ENDIF.
* Return the Instance
ro_ref = lo_ref.
Z_PROCESS_INBOUND_WORKFLOW->IF_INBOUND_EXIT_BCS~PROCESS_INBOUND
This method will be called automatically for the processing the message when it is received by the SAP system.
Sample Code Below:
* Declare for Inbound E-Mail processing
DATA: lo_document TYPE REF TO if_document_bcs,
l_mail_attr TYPE bcss_dbpa,
l_mail_content TYPE bcss_dbpc,
lv_reqno TYPE grac_reqno,
lv_approve_reject TYPE char1,
lt_cont_text TYPE soli_tab,
ls_cont_text TYPE soli,
lo_reply TYPE REF TO cl_send_request_bcs,
sender TYPE REF TO if_sender_bcs,
sender_addr TYPE string,
lv_email TYPE ad_smtpadr,
send_request TYPE REF TO cl_bcs,
lo_approval TYPE REF TO z_grac_approbation_by_email.
*--------------------------------------------------------------------*
*- Get a pointer to the reply email object -*
*--------------------------------------------------------------------*
TRY.
lo_reply = io_sreq->reply( ).
CATCH cx_send_req_bcs.
ENDTRY.
**** Check to make sure this is from an approved Sender
sender = io_sreq->get_sender( ).
sender_addr = sender->address_string( ).
lv_email = sender_addr.
TRANSLATE sender_addr TO UPPER CASE.
**** Only reply if this message came from within our mail system or domain
**** SPAMMERS Beware, your e-mails will not be processed!!!
IF sender_addr CS '@xxx.COM'.
**** send reply and inbound processing
*--------------------------------------------------------------------*
*- Get email subject -*
*--------------------------------------------------------------------*
TRY.
lo_document = io_sreq->get_document( ).
l_mail_attr = lo_document->get_body_part_attributes( '1' ).
*Get the request number from the desired position of the subject
lv_reqno = l_mail_attr-subject+12(10).
CATCH cx_document_bcs.
ENDTRY.
*--------------------------------------------------------------------*
*- Get mail body-*
*--------------------------------------------------------------------*
TRY.
l_mail_content = lo_document->get_body_part_content( '1' ).
lt_cont_text = l_mail_content-cont_text.
DELETE lt_cont_text WHERE line IS INITIAL.
READ TABLE lt_cont_text INTO ls_cont_text INDEX 1.
IF sy-subrc EQ 0.
TRANSLATE ls_cont_text-line TO UPPER CASE.
IF ls_cont_text-line+0(7) = 'APPROVE'.
lv_approve_reject = 'A'.
ELSEIF ls_cont_text-line+0(6) = 'REJECT'.
lv_approve_reject = 'R'.
ENDIF.
ENDIF.
CATCH cx_document_bcs.
ENDTRY.IF lv_approve_reject IS NOT INITIAL
AND lv_reqno IS NOT INITIAL
AND lv_email IS NOT INITIAL.CREATE OBJECT lo_approval
EXPORTING
i_reqno = lv_reqno
i_email = lv_email
i_approve_reject = lv_approve_reject.CALL METHOD lo_approval->process_request .
ENDIF.
ENDIF.
Now, I have created another class to validate approvers from their email addresses, process emails in case of any errors and finally start the approval process which is being called from above class method - Z_GRAC_APPROBATION_BY_EMAIL
First save the values in attributes of this class in the CONSTRUCTOR method.
Create a method PROCESS_REQUEST to do the processing.
In this method, the steps followed are:
- First get the SAP user ID for the email ID of the sender
- Validate by the SAP user ID, if the sender is actually the approver from checking tables GRFNMWRTINSTWI, GRACREQUSER
- If not, check if the sender is a delegate approver. You can user Function Module SAP_WAPI_SUBSTITUTIONS_GET
- If validated, create a background job using FM JOB_OPEN
The reason we need a background job is because the SY-UNAME in the system will be either WF-BATCH or a new user created by BASIS in the 1st step and that user is not the actual approver. So we create a background job and then change the user ID with the actual approver.
So, after the JOB_OPEN is called:
- Call FM BP_JOB_READ
- Change the user ID in Job Head and call FM BP_JOB_MODIFY
- We will have to create a new Report Program to approve or reject the request (Z_REP_APPROBATION_BY_EMAIL) and SUBMIT the program
- Call FM JOB_CLOSE
Now, the main logic is in the report program Z_REP_APPROBATION_BY_EMAIL.
I added 3 selection screen parameters to accept Request Number, BNAME(SAP User ID) of the approver and a field to identify Approve or Reject (A or R)
- First step is to fetch Request ID from Request Number from table GRACREQ. Concatenate 'ACCREQ/' and the Request ID togeather.
- Next is to fetch Work Item IDs for the Request Number from the table GRFNMWRTINSTWI
- After collecting data, we will call standard methods that GRC system uses to do the processing, Code Snippets are shown below:
go_session = cl_grfn_api_session=>open_daily( ).
TRY .
go_api ?= go_session->get( gv_reqid ).
gv_bname = p_bname.
CALL METHOD go_api->if_grac_api_access_request~retrieve
EXPORTING
iv_editable = abap_true
it_wi_id = gt_wi_id
iv_admin_mode = lv_bool
iv_approver_user = gv_bname.
IF p_aprj EQ 'A'.
ls_user_range-sign = 'I'.
ls_user_range-option = 'EQ'.
ls_user_range-low = gv_bname.
APPEND ls_user_range TO lt_user_range.
lv_user = gv_bname.
CALL METHOD cl_grac_user_rep=>retrieve_realtime_user
EXPORTING
iv_user = lv_user
IMPORTING
es_real_userinfo = ls_real_userinfo.
CALL METHOD cl_grac_user_rep=>retrieve_user_systems
EXPORTING
it_user = lt_user_range
* it_user_name =
* iv_max_rows = 1000
RECEIVING
rt_user = lt_user.
ls_val-val1 = ls_real_userinfo-department.
ls_val-val2 = ls_real_userinfo-location.
ls_val-val3 = ls_real_userinfo-company.
ls_val-val4 = ls_real_userinfo-costcenter.
ls_val1-val1 = ls_real_userinfo-userid.
ls_val1-val2 = ls_real_userinfo-user_group.
ls_val1-val3 = ls_real_userinfo-orgunit.
IF lt_user IS NOT INITIAL.
LOOP AT lt_user INTO ls_user.
ls_val1-val4 = ls_user-connector.
IF cl_grac_auth_engine=>authority_check(
iv_auth_obj = graca_c_emp-auth_obj
iv_field1 = graca_c_actvt-actvt
iv_value1 = graca_c_actvt-change
iv_field2 = graca_c_emp-dept
iv_value2 = ls_val-val1
iv_field3 = graca_c_emp-location
iv_value3 = ls_val-val2
iv_field4 = graca_c_emp-company
iv_value4 = ls_val-val3
iv_field5 = graca_c_emp-cost_centre
iv_value5 = ls_val-val4
) EQ abap_true AND
cl_grac_auth_engine=>authority_check(
iv_auth_obj = graca_c_user-auth_obj
iv_field1 = graca_c_actvt-actvt
iv_value1 = graca_c_actvt-change
iv_field2 = graca_c_user-userid
iv_value2 = ls_val1-val1
iv_field3 = graca_c_user-usergroup
iv_value3 = ls_val1-val2
iv_field4 = graca_c_user-org_unit
iv_value4 = ls_val1-val3
iv_field5 = graca_c_user-connector
iv_value5 = ls_val1-val4
) EQ abap_true.
lv_flg = 'X'.
EXIT.
ENDIF.
ENDLOOP.
ELSE.
ls_val1-val4 = ls_user-connector.
IF cl_grac_auth_engine=>authority_check(
iv_auth_obj = graca_c_emp-auth_obj
iv_field1 = graca_c_actvt-actvt
iv_value1 = graca_c_actvt-create
iv_field2 = graca_c_emp-dept
iv_value2 = ls_val-val1
iv_field3 = graca_c_emp-location
iv_value3 = ls_val-val2
iv_field4 = graca_c_emp-company
iv_value4 = ls_val-val3
iv_field5 = graca_c_emp-cost_centre
iv_value5 = ls_val-val4
) EQ abap_true AND
cl_grac_auth_engine=>authority_check(
iv_auth_obj = graca_c_user-auth_obj
iv_field1 = graca_c_actvt-actvt
iv_value1 = graca_c_actvt-create
iv_field2 = graca_c_user-userid
iv_value2 = ls_val1-val1
iv_field3 = graca_c_user-usergroup
iv_value3 = ls_val1-val2
iv_field4 = graca_c_user-org_unit
iv_value4 = ls_val1-val3
iv_field5 = graca_c_user-connector
iv_value5 = ls_val1-val4
) EQ abap_true.
lv_flg = 'X'.
ENDIF.
ENDIF.
IF lv_flg = 'X'.PERFORM f_fill_approving_details CHANGING ls_req_data
lt_item
lt_requser
lt_reqsys.lo_api ?= go_session->get( gv_reqid ).
CALL METHOD lo_api->if_grac_api_access_request~update
EXPORTING
is_request_data = ls_req_data
it_requser = lt_requser
it_reqlineitm = lt_item
it_reqsys = lt_reqsys.CALL METHOD go_session->save.
ENDIF.
ELSEIF p_aprj EQ 'R'.
CALL METHOD go_api->if_grac_api_access_request~reject .CALL METHOD go_session->save.
ENDIF.
CATCH cx_grfn_exception INTO go_grfn_exp.
ENDTRY.
*&---------------------------------------------------------------------*
*& Form f_fill_approving_details
*&---------------------------------------------------------------------*
* text
*----------------------------------------------------------------------*
* -->LS_REQ_DATA text
*----------------------------------------------------------------------*
FORM f_fill_approving_details CHANGING ps_req_data TYPE grac_s_api_req_data
pt_item TYPE grac_t_api_reqlineitem
pt_requser TYPE grac_t_api_user_info
pt_reqsys TYPE grac_t_api_reqsys.TYPES: BEGIN OF ty_gracreq,
req_id TYPE grfn_guid,
req_created TYPE grac_req_created,
duedate TYPE grac_duedate,
reqtype TYPE grac_reqtype,
funcarea TYPE grac_funarea,
msmp_process_id TYPE grfn_mw_process_id,
END OF ty_gracreq,BEGIN OF ty_gracitem,
itemnum TYPE grac_seq,
connector TYPE grac_reqsystem,
prov_item_id TYPE grfn_guid,
prov_item_type TYPE grac_prov_item_type,
prov_action TYPE grac_actiontype,
prov_item_name TYPE grac_prov_item_name,
approval_status TYPE grac_approval_status,
valid_from TYPE grac_valid_from,
valid_to TYPE grac_valid_to,
prov_type TYPE grac_prov_type,
END OF ty_gracitem,BEGIN OF ty_systems,
systems TYPE grfn_connectorid,
END OF ty_systems.DATA: lv_reqid TYPE grfn_guid,
ls_gracreq TYPE ty_gracreq,
lt_gracitem TYPE STANDARD TABLE OF ty_gracitem,
ls_gracitem TYPE ty_gracitem,
lt_gracuser TYPE STANDARD TABLE OF gracrequser,
ls_gracuser TYPE gracrequser,
ls_reqsys TYPE grac_s_api_reqsys,
lt_systems TYPE STANDARD TABLE OF ty_systems,
ls_systems TYPE ty_systems,
ls_requser TYPE grac_s_api_user_info,
ls_item TYPE grac_s_api_reqlineitem.lv_reqid = gv_reqid+7.
SELECT SINGLE req_id
req_created
duedate
reqtype
funcarea
msmp_process_id
FROM gracreq
INTO ls_gracreq
WHERE req_id = lv_reqid.
IF sy-subrc EQ 0.
ps_req_data-req_id = ls_gracreq-req_id.
ps_req_data-req_created = ls_gracreq-req_created.
ps_req_data-req_approved = ls_gracreq-duedate.
ps_req_data-reqtype = ls_gracreq-reqtype.
ps_req_data-msmp_process_id = ls_gracreq-msmp_process_id.
ps_req_data-funcarea = ls_gracreq-funcarea.SELECT itemnum
connector
prov_item_id
prov_item_type
prov_action
prov_item_name
approval_status
valid_from
valid_to
prov_type
FROM gracreqprovitem
INTO TABLE lt_gracitem
WHERE req_id = lv_reqid.IF sy-subrc EQ 0.
LOOP AT lt_gracitem INTO ls_gracitem.
ls_item-itemnum = ls_gracitem-itemnum.
ls_item-item_name = ls_gracitem-prov_item_name.
ls_item-connector = ls_gracitem-connector.
ls_item-prov_item_id = ls_gracitem-prov_item_id.
ls_item-prov_item_type = ls_gracitem-prov_item_type.
ls_item-prov_action = ls_gracitem-prov_action.
ls_item-approval_status = 'AP'.
ls_item-valid_from = ls_gracitem-valid_from.
ls_item-valid_to = ls_gracitem-valid_to.
ls_item-prov_type = ls_gracitem-prov_type.APPEND ls_item TO pt_item.
ENDLOOP.
ENDIF.SELECT * FROM gracrequser
INTO TABLE lt_gracuser
WHERE req_id = lv_reqid.IF sy-subrc EQ 0.
LOOP AT lt_gracuser INTO ls_gracuser.
ls_requser-userid = ls_gracuser-userid.
ls_requser-provuser = ls_gracuser-provuser.
ls_requser-snc_name = ls_gracuser-snc_name.
ls_requser-unsec_snc = ls_gracuser-unsec_snc.
ls_requser-accno = ls_gracuser-accno.
ls_requser-empposition = ls_gracuser-empposition.
ls_requser-empjob = ls_gracuser-empjob.
ls_requser-personnelno = ls_gracuser-personnelno.
ls_requser-personnelarea = ls_gracuser-personnelarea.
ls_requser-email = ls_gracuser-email.
ls_requser-emptype = ls_gracuser-emptype.
ls_requser-logon_langu = ls_gracuser-logon_langu.
ls_requser-dec_notation = ls_gracuser-dec_notation.
ls_requser-date_format = ls_gracuser-date_format.
ls_requser-time_zone = ls_gracuser-time_zone.
ls_requser-manager = ls_gracuser-manager.
APPEND ls_requser TO pt_requser.ENDLOOP.
ENDIF.SELECT systems
FROM gracrequsersys
INTO TABLE lt_systems
WHERE req_id = lv_reqid.IF sy-subrc EQ 0.
LOOP AT lt_systems INTO ls_systems.
ls_reqsys-systems = ls_systems-systems.
APPEND ls_reqsys TO pt_reqsys.
ENDLOOP.
ENDIF.ENDIF.
ENDFORM. "f_fill_approving_details